使用Frida对Adobe Reader进行动态插桩分析

本文详细介绍了如何使用Frida工具对Adobe Reader进行动态插桩分析,包括函数钩子、内存操作、参数修改、跨进程通信以及代码追踪等高级技术,并提供了实际代码示例和架构解析。

使用Frida对Adobe Reader进行动态插桩分析

Frida是一款近年来流行的开源动态插桩工具包,在移动安全领域尤其常见。本文将对这一工具进行基本介绍,并展示如何在Windows平台上使用它。

安装Frida

安装Frida非常简单,可以使用Python包管理器pip完成。确认系统已安装Python和pip后,运行以下命令:

1
$ pip install frida-tools

提示:如果在Windows上遇到错误,请先运行pip install wheel,然后重新执行上述命令。

验证安装是否成功,可在Python REPL中输入import frida,若无错误信息则安装正确。

函数钩子

Frida最基本的功能之一是钩取运行中进程的函数调用。以Adobe Acrobat Reader为例,尝试拦截所有JavaScript控制台输出(示例使用Adobe Acrobat 2020.012.20043版本的偏移量,其他版本可能无效)。

创建脚本:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import frida
import sys

def on_message(message, data):
    print("[%s] => %s" % (message, data))

def main(target_process):
    session = frida.attach(target_process)
    script = session.create_script('''
        var EScript_base = Module.findBaseAddress('EScript.api');
        console.log('EScript.api baseAddr: ' + EScript_base);
        var print2console_addr = EScript_base.add(ptr('0x824c7'));
        
        function getCASText(addr) {
            if (addr.isNull())
                return '';
            return addr.add(4).readPointer().readUtf16String();
        }
        
        Interceptor.attach(print2console_addr, {
            onEnter: function(args) {
                console.log('');
                console.log('[+] Called console.print ' + print2console_addr);
                var text = getCASText(args[0]);
                console.log(text);
            }
        });
    ''')
    script.on('message', on_message)
    script.load()
    print("[!] Ctrl+D on UNIX, Ctrl+Z on Windows/cmd.exe to detach from instrumented program.\n\n")
    sys.stdin.read()
    session.detach()

if __name__ == '__main__':
    if len(sys.argv) != 2:
        print("Usage: %s <process name or PID>" % __file__)
        sys.exit(1)
    try:
        target_process = int(sys.argv[1])
    except ValueError:
        target_process = sys.argv[1]
    main(target_process)

Frida架构

Frida主要由两部分组成:Python代码在独立进程中运行,通过绑定层(如frida-python)与frida-core通信;frida-agent直接注入目标进程(如Adobe Acrobat)并执行JavaScript。上述更改将JavaScript代码发送到frida-agent,使其在目标进程内运行,on_message处理程序为JavaScript组件提供向工具进程返回数据的途径。

Frida JavaScript API

Frida JavaScript API文档完善,推荐在使用时参考官方文档。以上脚本中使用的API包括:

  • Module.findBaseAddress(name):返回模块基地址
  • 指针操作:使用.add.sub
  • 解引用方法:如.readPointer.readUtf16String.readByteArray.readU8
  • Interceptor.attach(target, callbacks[, data]):拦截目标函数调用,可指定onEnteronLeave回调

运行代码后,可成功拦截所有JavaScript控制台输出。

内存分配和参数操作

除了观察函数调用,还可以操作它们。例如,钩取JavaScript评估函数并修改要运行的代码(需自行查找偏移量):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
var EScript_base = Module.findBaseAddress('EScript.api');
console.log('EScript.api baseAddr: ' + EScript_base);
var eval_func_addr = EScript_base.add(ptr('0x7f3eb')); // 函数偏移量

function getUTF16String(addr) {
    var end = addr;
    while (end.readU16() !== 0) end = end.add(2);
    var size = end.sub(addr).toInt32();
    console.log("size: "+size);
    var buf = addr.add(2).readByteArray(size-2);
    buf = new Uint16Array(buf);
    console.log(buf);
    var out = '';
    for (var i = 0; i < (size-2)/2; i++) {
        var c = ((buf[i]&0xff)<<8)+(buf[i]>>8);
        out += String.fromCharCode(c);
    }
    return out;
}

function createUTF16String(str) {
    var size = str.length*2 + 4+4;
    var buf = Memory.alloc(size);
    var ret = buf;
    buf.writeU8(0xfe); buf = buf.add(1);
    buf.writeU8(0xff); buf = buf.add(1);
    for (var i = 0; i < str.length; i++) {
        var c = str.charCodeAt(i);
        buf.writeU8(c>>8); buf = buf.add(1);
        buf.writeU8(c&0xff); buf = buf.add(1);
    }
    buf.writeU16(0); buf = buf.add(2);
    buf.writeU16(0); buf = buf.add(2);
    buf.writeU16(0); buf = buf.add(2);
    return ret;
}

Interceptor.attach(eval_func_addr, {
    onEnter: function(args) {
        console.log('');
        console.log('[+] Called eval func ' + eval_func_addr);
        var code = getUTF16String(args[0])
        console.log('Old code: '+code);
        var new_code = createUTF16String(code.replace('Adobe', 'Frida'));
        this.new_code = new_code;
        args[0] = new_code;
    }
});

由于Adobe使用大端序UTF16,需自定义编码和解码函数。注意:

  • Memory.alloc(size):在堆上分配size字节内存,返回缓冲区指针
  • this.new_code = new_code;:必要,因为Frida分配的缓冲区在没有js指针指向时会自动释放
  • 每个.readU8.readPointer等都有对应的写入版本
  • 可编辑args变量以操作函数参数

运行代码后,所有“Adobe”一词在执行前被替换为“Frida”。

跨进程消息传递和实际示例

接下来看一个更实际和复杂的示例,涉及将数据发送回Python代码。钩取Adobe JavaScript内置函数使用的参数解析函数ESArgParse,为内部结构实现解析逻辑,并将结果发送回Python代码。

JavaScript组件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
var EScript_base = Module.findBaseAddress('EScript.api');
console.log('EScript.api baseAddr: ' + EScript_base);
var ESArgParse_addr = EScript_base.add(ptr('0x55f20')); // 函数偏移量

Interceptor.attach(ESArgParse_addr, {
    onEnter: function(args) {
        console.log('');
        console.log('[+] Called ESArgParse ' + ESArgParse_addr);
        var struct = parseParamStruct(args[0]);
        console.log(struct);
        send(struct);
    }
});

function parseParamStruct(addr) {
    var out = [];
    while(!addr.readPointer().isNull()) {
        var paramName = addr.readPointer().readCString();
        var paramType = addr.add(4).readU32();
        var isOptional = addr.add(8).readU32();
        var resultPtr = addr.add(12).readU32();
        var emptyField = addr.add(16).readU32();
        out.push([paramName, paramType, isOptional, resultPtr, emptyField]);
        addr = addr.add(5*4);
    }
    return out;
}

send函数用于将数据发送回Python代码。

Python代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import frida
import sys

def get_type(v):
    return ['boolean', 'number', 'object', 'string', 'unknown', 'value', 'string'][v] if v <= 6 else 'unknown'

def on_message(message, data):
    print("[%s] => %s" % (message, data))
    if message['payload']:
        for param in message['payload']:
            print('parameter {:10} has type {:6}, is {:8}, and should be stored at {:8}'.format(
                param[0], get_type(param[1]), 'required' if param[2] == 0 else 'optional', hex(param[3])))

def main(target_process):
    session = frida.attach(target_process)
    script = session.create_script('''
        // 上述JavaScript代码
    ''')
    script.on('message', on_message)
    script.load()
    print("[!] Ctrl+D on UNIX, Ctrl+Z on Windows/cmd.exe to detach from instrumented program.\n\n")
    sys.stdin.read()
    session.detach()

if __name__ == '__main__':
    if len(sys.argv) != 2:
        print("Usage: %s <process name or PID>" % __file__)
        sys.exit(1)
    try:
        target_process = int(sys.argv[1])
    except ValueError:
        target_process = sys.argv[1]
    main(target_process)

此次不同之处在于在on_message处理程序中访问message['payload']属性。Frida可处理JavaScript对象到Python对象的转换,直接访问从JavaScript端发送的数据。

运行代码并在控制台执行app.alert('hi'),即可获得输出。

函数追踪和Frida Stalker

Frida还有一个强大的代码追踪引擎Stalker,类似于DynamoRIO和Pin,能够捕获执行的每个函数、块甚至指令,并支持动态代码重编译。Stalker的特殊之处在于包含JavaScript绑定,允许用高级语言编程逻辑,并在需要性能的部分使用CModules回退到低级C代码。

入门Stalker的好资源包括官方文档和详细概述。由于Stalker本身是一个庞大主题,本文仅提供一个简单示例,省略一些高级功能。基于上一个示例,查看ESArgParse调用的函数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
var EScript_base = Module.findBaseAddress('EScript.api');
console.log('EScript.api baseAddr: ' + EScript_base);
var ESArgParse_addr = EScript_base.add(ptr('0x55f20')); // 函数偏移量

Interceptor.attach(ESArgParse_addr, {
    onEnter: function(args) {
        console.log('');
        console.log('[+] Called ESArgParse ' + ESArgParse_addr);
        var struct = parseParamStruct(args[0]);
        send(struct);
        Stalker.follow({
            events: {
                call: true, // 仅关注函数调用
                ret: false,
                exec: false,
                block: false,
                compile: false
            },
            onReceive: function (events) {
                var calls = Stalker.parse(events, {
                    annotate: true, // 显示事件类型
                });
                for (var i = 0; i < calls.length; i++) {
                    var call = calls[i];
                    if (call[0] !== 'call') break;
                    console.log((' '.repeat(call[3]*2))+'↳ calling '+call[2]);
                }  
            },
        })
    },
    onLeave: function(ret_val) {
        Stalker.unfollow();
    }
});

function parseParamStruct(addr) {
    var out = [];
    while(!addr.readPointer().isNull()) {
        var paramName = addr.readPointer().readCString();
        var paramType = addr.add(4).readU32();
        var a = addr.add(8).readU32();
        var b = addr.add(12).readU32();
        var c = addr.add(16).readU32();
        out.push([paramName, paramType, a, b, c]);
        addr = addr.add(5*4);
    }
    return out;
}

使用Stalker的新方法包括:

  • 通过Stalker.followStalker.unfollow随时启用或禁用Stalker,最小化追踪性能影响
  • onReceive回调的events参数为二进制格式,需使用Stalker.parse解析
  • 不同事件有不同结构,可参考gumevent.h

运行代码后,获得嵌套函数调用栈,帮助直观识别有趣函数和程序逻辑。

结论

本文介绍了Frida的基础知识,包括安装、函数钩子、参数操作和跨进程消息传递,并解释了一些内部架构思想。此外,还简要介绍了强大的内置代码追踪器Stalker及其功能。全文伴随相关示例,展示了Frida的使用思路。

最后,以下表格比较Frida与其他工具,显示它是传统调试器脚本和其他动态二进制插桩框架之间的良好折衷:

特性 Frida 传统调试器脚本(如pykd、winappdbg) DBI框架(如DynamoRIO、PIN)
高级语言脚本
快速代码追踪
动态代码重编译
单一语言接口
易于设置

希望本文能激励读者尝试Frida,并了解它如何有益于工作流程。

comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计