Frida调用栈符号恢复

By xia0

Frida调用栈符号恢复

开始

搞了一阵子的Android方向的逆向研究,发现用frida去做一些hook等测试十分方便。最近自己想在iOS平台实现短视频下载去水印的问题,所以也想用frida来试试。但是测试过程中当我想去看下hook点的调用栈的时候,发现只有一些栈地址,基本没有符号信息,估计符号表被strip了。这里通常的做法就是用这些地址减去偏移然后去ida里面找到对应的方法。这样无疑是大大增加了分析的时间,想到之前解决lldb调试器栈符号恢复)方案,决定把lldb的栈符号恢复脚本移植到frida中。

如何恢复已经去掉符号表的可执行文件?

这里的符号恢复仅仅针对的是OC函数,C函数如果符号表被strip以后是没有办法恢复其符号信息的。为什么OC函数可以去做符号恢复呢?这里要涉及到macho文件的格式以及ObjectC这个语言自身设计相关。可以看到在macho文件中的_DATA数据段中有很多objc的节信息,里面保存了所有的类以及方法等元数据信息。既然如此,我们肯定能找到方法去恢复这些OC函数的符号。

image-20190702172303432

OC函数符号恢复思路

首先我们只能得到一堆调用链的地址,这些地址肯定是函数里面的某个偏移地址。很容易想到这个地址往前推肯定就是这个函数的首地址及函数地址。如果我们拿到了所有函数的地址,然后每一个地址和目标地址比较,与目标地址距离最近的那个地址所对应的函数不就是我们想要的符号吗。

根据上面提到的思路,目前需要解决几个问题,怎么拿到所有OC方法的地址? 以及对应的类名和方法名?如何设计匹配算法等?

这里有两种办法:

  • 第一种是自己去解析在内存中加载的macho文件,根据macho的文件格式先找到class信息,然后找到对应的method信息,method里面就保存了IMP和方法名。之前我尝试这样去过,所有的信息都能拿到,但是由于在macho在加载到内存的时候objc动态库会做很多的初始化等工作,导致要处理一些细节问题,所以就没继续做了。
  • 第二种是利用已有的objc提供的接口objc_去拿到所有的class以及对应method的方法名和IMP。这里主要用的的API有objc_copyClassNamesForImageclass_copyMethodListobjc_getClassmethod_getImplementationmethod_getNameobjc_getClassobjc_getMetaClass

现在已经能拿到所有的类方法、方法名、方法实现地址了,接下来要解决的就是怎么通过调用栈的地址去找到对应的方法,这里的思路就是遍历所有的方法地址与调用栈的地址比较并计算距离,如果方法地址小于目标地址且距离最小,那么该方法就是我们要找到的符号。最后将调用栈上面的所有地址都进行该操作即可。

frida的js环境编写代码

由于我之前在lldb的python脚本中写过该过程代码(lldb内置的OC解释器语法要求十分严格,调试了很久的代码)

按照上面的思路理论上代码很好写,也不是很复杂。如果是直接写OC代码应该很好写,但是在frida中写这些还是挺折腾的。

主要的代码如下:

根据模块路径获取其所有的类

function getAllClass(modulePath){

    // const char * objc_copyClassNamesForImage(const char *image, unsigned int *outCount)
    var objc_copyClassNamesForImage = new NativeFunction(
        Module.findExportByName(null, 'objc_copyClassNamesForImage'),
        'pointer',
        ['pointer', 'pointer']
    );
    // free
    var free = new NativeFunction(Module.findExportByName(null, 'free'), 'void', ['pointer']);

    // if given modulePath nil, default is mainBundle
    if(!modulePath){
        var path = ObjC.classes.NSBundle.mainBundle().executablePath().UTF8String();
    }else{
        var path = modulePath;
    }

    // create args
    var pPath = Memory.allocUtf8String(path);
    var p = Memory.alloc(Process.pointerSize);
    Memory.writeUInt(p, 0);

    var pClasses = objc_copyClassNamesForImage(pPath, p);
    var count = Memory.readUInt(p);
    var classes = new Array(count);

    for (var i = 0; i < count; i++) {
        var pClassName = Memory.readPointer(pClasses.add(i * Process.pointerSize));
        classes[i] = Memory.readUtf8String(pClassName);
    }

    free(pClasses);

    // XLOG(classes)
    return classes;
}

根据类名获取所有的方法信息,由于有实例方法和类方法,这里需要分别获取。

function getAllMethods(classname){
    var objc_getClass = new NativeFunction(
        Module.findExportByName(null, 'objc_getClass'),
        'pointer',
        ['pointer']
    );
    var class_copyMethodList = new NativeFunction(
        Module.findExportByName(null, 'class_copyMethodList'),
        'pointer',
        ['pointer', 'pointer']
    );

    var objc_getMetaClass = new NativeFunction(
        Module.findExportByName(null, 'objc_getMetaClass'),
        'pointer',
        ['pointer']
    );

    var method_getName = new NativeFunction(
        Module.findExportByName(null, 'method_getName'),
        'pointer',
        ['pointer']
    );

    var free = new NativeFunction(Module.findExportByName(null, 'free'), 'void', ['pointer']);

    // get objclass and metaclass
    var name = Memory.allocUtf8String(classname);
    var objClass = objc_getClass(name)
    var metaClass = objc_getMetaClass(name)

    // get obj class all methods
    var size_ptr = Memory.alloc(Process.pointerSize);
    Memory.writeUInt(size_ptr, 0);
    var pObjMethods = class_copyMethodList(objClass, size_ptr);
    var count = Memory.readUInt(size_ptr);

    var allMethods = new Array();

    var allObjMethods = new Array();

    // get obj class all methods name and IMP
    for (var i = 0; i < count; i++) {
        var curObjMethod = new Array();

        var pObjMethodSEL = method_getName(pObjMethods.add(i * Process.pointerSize))
        var pObjMethodName = Memory.readCString(Memory.readPointer(pObjMethodSEL))
        var objMethodIMP = Memory.readPointer(pObjMethodSEL.add(2*Process.pointerSize))
        // XLOG("-["+classname+ " " + pObjMethodName+"]" + ":" + objMethodIMP)
        curObjMethod.push(pObjMethodName)
        curObjMethod.push(objMethodIMP)
        allObjMethods.push(curObjMethod)
    }

    var allMetaMethods = new Array();

    // get meta class all methods name and IMP
    var pMetaMethods = class_copyMethodList(metaClass, size_ptr);
    var count = Memory.readUInt(size_ptr);
    for (var i = 0; i < count; i++) {
        var curMetaMethod = new Array();

        var pMetaMethodSEL = method_getName(pMetaMethods.add(i * Process.pointerSize))
        var pMetaMethodName = Memory.readCString(Memory.readPointer(pMetaMethodSEL))
        var metaMethodIMP = Memory.readPointer(pMetaMethodSEL.add(2*Process.pointerSize))
        //XLOG("+["+classname+ " " + pMetaMethodName+"]" + ":" + metaMethodIMP)
        curMetaMethod.push(pMetaMethodName)
        curMetaMethod.push(metaMethodIMP)
        allMetaMethods.push(curMetaMethod)
    }

    allMethods.push(allObjMethods)
    allMethods.push(allMetaMethods)

    free(pObjMethods);
    free(pMetaMethods);

    return allMethods;
}

通过调用栈地址根据最近匹配的算法去找到对应的符号信息

function findSymbolFromAddress(modulePath,addr){
    var frameAddr = addr

    var theDis = 0xffffffffffffffff;
    var tmpDis = 0;
    var theClass = "None"
    var theMethodName = "None"
    var theMethodType = "-"
    var theMethodIMP = 0

    var allClassInfo = {}

    var allClass = getAllClass(modulePath);

    for(var i = 0, len = allClass.length; i < len; i++){
        var mInfo = getAllMethods(allClass[i]);
        var curClassName = allClass[i]

        objms = mInfo[0];
        for(var j = 0, olen = objms.length; j < olen; j++){
            mname = objms[j][0]
            mIMP = objms[j][1]
            if(frameAddr >= mIMP){
                tmpDis = frameAddr-mIMP
                if(tmpDis < theDis){
                    theDis = tmpDis
                    theClass = curClassName
                    theMethodName = mname
                    theMethodIMP = mIMP
                    theMethodType = "-"
                }
            }
        }

        metams = mInfo[1];
        for(var k = 0, mlen = metams.length; k < mlen; k++){
            mname = metams[k][0]
            mIMP = metams[k][1]
            if(frameAddr >= mIMP){
                tmpDis = frameAddr-mIMP
                if(tmpDis < theDis){
                    theDis = tmpDis
                    theClass = curClassName
                    theMethodName = mname
                    theMethodIMP = mIMP
                    theMethodType = "+"
                }
            }
        }
    }

    symbol = theMethodType+"["+theClass+" "+theMethodName+"]"

    if(symbol.indexOf(".cxx")!=-1){
        symbol = "maybe C function?"
    }

    // if distance > 3000, maybe a c function
    if(theDis > 3000){
        symbol = "maybe C function? symbol:" + symbol
    }

    return symbol;
}

在匹配算法的最后还进行了一些判断,当解析出来的方法名包含.cxx方法的时候说明没找到符号,可能是一个C函数。当解析出来的方法地址距离目标地址距离大于3000的时候会提示可能会C函数。

最后完整的项目地址:https://github.com/4ch12dy/xia0FridaScript

测试

我这里写了一个简单的frida脚本去测试如何导入符号恢复的js脚本

#!/usr/bin/python
import frida
import sys 
import codecs
import os

PACKAGE_NAME = "cn.xiaobu.pipiPlay"

def on_message(message, data):
    try:
        if message:
            print("[JSBACH] {0}".format(message["payload"]))
    except Exception as e:
        print(message)
        print(e)

def xia0CallStackSymbolsTest():
    script_dir = os.path.dirname(os.path.realpath(__file__))
    xia0CallStackSymbolsJS = os.path.join(script_dir, 'xia0CallStackSymbols.js')
    source = ''
    with codecs.open(xia0CallStackSymbolsJS, 'r', 'utf-8') as f:
        source = source + f.read()

    js = '''
    if (ObjC.available)
    {
            try
            {
                    //Your class name here  - ZYOperationView operationCopyLink
                    var className = "ZYMediaDownloadHelper";
                    //Your function name here
                    var funcName = "+ downloadMediaUrl:isVideo:progress:finishBlock:";
                    var hook = eval('ObjC.classes.' + className + '["' + funcName + '"]');
                    Interceptor.attach(hook.implementation, {
                        onEnter: function(args) {
                            // args[0] is self
                            // args[1] is selector (SEL "sendMessageWithText:")
                            // args[2] holds the first function argument, an NSString
                            console.log("[*] Detected call to: " + className + " -> " + funcName);

                            // just call [NSThread callStackSymbols]
                            var threadClass = ObjC.classes.NSThread
                            var symbols = threadClass["+ callStackSymbols"]()
                            console.log(symbols)

                            // call  xia0CallStackSymbols [true:just symbolish mainModule address false:symbolish all module address]
                            xia0CallStackSymbols(true);
                            xia0CallStackSymbols(false);
                        }
                    });
            }
            catch(err)
            {
                    console.log("[!] Exception2: " + err.message);
            }
    }
    else
    {
            console.log("Objective-C Runtime is not available!");
    }
    '''

    return source+js


def do_hook():
    return xia0CallStackSymbolsTest()

if __name__ == '__main__':
    try:
        device = frida.get_device_manager().enumerate_devices()[-1]
        print device
        pid = device.spawn([PACKAGE_NAME])
        print("[JSBACH] {} is starting. (pid : {})".format(PACKAGE_NAME, pid))

        session = device.attach(pid)
        device.resume(pid)

        script = session.create_script(do_hook())
        script.on('message', on_message)
        script.load()
        sys.stdin.read()
    except KeyboardInterrupt:
        sys.exit(0)

只需要将xia0CallStackSymbols.js脚本放到项目中,然后用以下代码即可导入使用

script_dir = os.path.dirname(os.path.realpath(__file__))
xia0CallStackSymbolsJS = os.path.join(script_dir, 'xia0CallStackSymbols.js')
source = ''
with codecs.open(xia0CallStackSymbolsJS, 'r', 'utf-8') as f:
    source = source + f.read()

your_frida_js_hook_script = ""
load_js = your_frida_js_hook_script+source

恢复的效果如下:

  • 通过[NSThread callStackSymbols]获取的调用栈符号

    image-20190702181519237

  • 通过调用xia0CallStackSymbols函数获取的调用栈符号

    image-20190702181145710

  • 作为比较,我在放上lldb中恢复调用栈

    image-20190702181638614

这里可以看出lldb调试器恢复的符号信息最完整且准确,lldb的栈符号恢复项目在这里,现在还能支持block函数的符号恢复。

这里有几个问题需要说明一下:

  • xia0CallStackSymbols的符号为什么前15个地址没有显示?

    因为前15个地址都是frida中js解释器里面的函数执行地址,没有办法拿到模块信息,也没必要解析这些地址。

  • xia0CallStackSymbols中还提供了内存对应的文件地址,如果你觉得符号有问题,可以直接去ida中手动查找符号

  • 如果用dladdr能够拿到地址的符号信息,就没有调用xia0CallStackSymbols去恢复(比如符号表没有strip的情况)

  • xia0CallStackSymbols()接口可以传递一个bool参数,true为仅仅解析主模块的地址,false为所有模块都需要解析。实际在逆向过程中一般只需要主模块的符号信息,其他系统函数没很大必要。

遗留问题/Todo

  • 在执行恢复符号的过程中时间相对较长,主要原因在于每一个地址都要和所有方法比较,这里建议xia0CallStackSymbols传入true,这样只解析主模块的地址。耗时的原因还在于每一个地址解析的时候都会去调用接口获取所有方法信息,实际上每个模块只需要一次就能拿到所有方法信息,接下来要做的就是优化相关代码,缓存模块的所有方法信息,下次解析的地址为该模块时直接去缓存里面匹配查找。
  • 在匹配符号的过程中,判断是否为C函数需要更多的原则,3000的阈值需要后面再调下。
  • 关于block的符号恢复,目前只有在lldb中实现了,下一步准备在xia0CallStackSymbols中也支持恢复block函数符号

题外话

短视频下载去水印如果有人感兴趣的话,可以点这里,目前支持的有皮皮搞笑、抖音、皮皮虾、Tiktok

参考