首页
论坛
课程
招聘
[分享][原创]Unidbg模拟执行so
2021-8-24 14:05 3070

[分享][原创]Unidbg模拟执行so

2021-8-24 14:05
3070

在这里插入图片描述

 

Android逆向之ARM64静态分析 对ARM64汇编进行了介绍,网传ARMV9要出来了,难道又要重新学习ARMV9? 在Frida高级篇-免ROOT使用Frida(不修改源代码) 中对elf文件进行了介绍,本文使用unidbg模拟执行so来分析native方法。首先来介绍Unicorn。

Unicorn

Unicorn is a lightweight multi-platform, multi-architecture CPU emulator framework.

 

本文使用无名侠大神使用的Unicorn入门教程来看看Unicorn是怎么模拟CPU的。

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
from unicorn import *
from unicorn.arm_const import *
from capstone import *
 
ARM_CODE = b"\x37\x00\xa0\xe3\x03\x10\x42\xe0"
 
# Disassemble ARM32 binary
md = Cs(CS_ARCH_ARM, CS_MODE_ARM)
for i in md.disasm(ARM_CODE, 0x1000):
    print("0x%x:\t%s\t%s" %(i.address, i.mnemonic, i.op_str))
 
 
# mov r0, #0x37;
# sub r1, r2, r3
# Test ARM
 
# callback for tracing instructions
def hook_code(uc, address, size, user_data):
    print(">>> Tracing instruction at 0x%x, instruction size = 0x%x" % (address, size))
 
def test_arm():
    print("Emulate ARM code")
    try:
        # Initialize emulator in ARM mode
        mu = Uc(UC_ARCH_ARM, UC_MODE_THUMB)
 
        # map 2MB memory for this emulation
        ADDRESS = 0x10000
        mu.mem_map(ADDRESS, 2 * 0x10000)
        mu.mem_write(ADDRESS, ARM_CODE)
 
        mu.reg_write(UC_ARM_REG_R0, 0x1234)
        mu.reg_write(UC_ARM_REG_R2, 0x6789)
        mu.reg_write(UC_ARM_REG_R3, 0x3333)
 
        mu.hook_add(UC_HOOK_CODE, hook_code, begin=ADDRESS, end=ADDRESS+8)
        # emulate machine code in infinite time
        mu.emu_start(ADDRESS, ADDRESS + len(ARM_CODE))
        r0 = mu.reg_read(UC_ARM_REG_R0)
        r1 = mu.reg_read(UC_ARM_REG_R1)
        print(">>> R0 = 0x%x" % r0)
        print(">>> R1 = 0x%x" % r1)
    except UcError as e:
        print("ERROR: %s" % e)
 
 
test_arm()

运行结果:
在这里插入图片描述

添加指令级的Hook

这个有点像单步调试的感觉。

1
mu.hook_add(UC_HOOK_CODE, hook_code, begin=ADDRESS, end=ADDRESS)

在begin...end范围内的每一条指令被执行前都会调用callback。

 

让我们来看看hook_code 的实现吧

1
2
3
# callback for tracing instructions
def hook_code(uc, address, size, user_data):
    print(">>> Tracing instruction at 0x%x, instruction size = 0x%x" %(address, size))

这段代码仅打印指令执行的地址和长度信息。 实际应用中可配合capstone反汇编引擎玩一些更骚的操作。

 

UC_HOOK_CODE的callback中可以修改PC或EIP等寄存器来改变程序运行流程。实际上,Unicorn调试器的单步调试就是以这个为基础实现的。

Unidbg

Allows you to emulate an Android native library, and an experimental iOS emulation.

 

下载代码: https://github.com/zhkl0228/unidbg/releases/tag/v0.9.3,使用IntelliJ IDEA打开工程即可。

 

运行代码: com/bytedance/frameworks/core/encrypt/TTEncrypt.java, 出现下面的信息说明运行成功。
在这里插入图片描述

代码分析

入口点:

1
2
3
4
5
6
7
8
public static void main(String[] args) throws Exception {
    TTEncrypt test = new TTEncrypt(true);
 
    byte[] data = test.ttEncrypt();
    Inspector.inspect(data, "ttEncrypt");
 
    test.destroy();
}

第一步: 补环境
跟踪TTEncrypt函数,注释写的很清楚了,不做过多分析。基本套路都是这个样子。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
TTEncrypt(boolean logging) {
    this.logging = logging;
 
    emulator = AndroidEmulatorBuilder.for32Bit().setProcessName("com.qidian.dldl.official").build(); // 创建模拟器实例,要模拟32位或者64位,在这里区分
    final Memory memory = emulator.getMemory(); // 模拟器的内存操作接口
    memory.setLibraryResolver(new AndroidResolver(23)); // 设置系统类库解析
 
    vm = emulator.createDalvikVM(null); // 创建Android虚拟机
    vm.setVerbose(logging); // 设置是否打印Jni调用细节
    DalvikModule dm = vm.loadLibrary(new File("unidbg-android/src/test/resources/example_binaries/libttEncrypt.so"), false); // 加载libttEncrypt.so到unicorn虚拟内存,加载成功以后会默认调用init_array等函数
    dm.callJNI_OnLoad(emulator); // 手动执行JNI_OnLoad函数
    module = dm.getModule(); // 加载好的libttEncrypt.so对应为一个模块
 
    TTEncryptUtils = vm.resolveClass("com/bytedance/frameworks/core/encrypt/TTEncryptUtils");
}

第二步: HOOK相关的函数
跟踪ttEncrypt,可知代码hook了ss_encrypt和ss_encrypted_size两个函数。

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
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
byte[] ttEncrypt() {
    if (logging) {
        Symbol sbox0 = module.findSymbolByName("sbox0"); // 在libttEncrypt.so模块中查找sbox0导出符号
        Symbol sbox1 = module.findSymbolByName("sbox1");
        Inspector.inspect(sbox0.createPointer(emulator).getByteArray(0, 256), "sbox0"); // 打印sbox0导出符号在unicorn中的内存数据
        Inspector.inspect(sbox1.createPointer(emulator).getByteArray(0, 256), "sbox1");
 
        IHookZz hookZz = HookZz.getInstance(emulator); // 加载HookZz,支持inline hook,文档看https://github.com/jmpews/HookZz
        hookZz.enable_arm_arm64_b_branch(); // 测试enable_arm_arm64_b_branch,可有可无
        hookZz.wrap(module.findSymbolByName("ss_encrypt"), new WrapCallback<RegisterContext>() { // inline wrap导出函数
            @Override
            public void preCall(Emulator<?> emulator, RegisterContext ctx, HookEntryInfo info) {
                Pointer pointer = ctx.getPointerArg(2);
                int length = ctx.getIntArg(3);
                byte[] key = pointer.getByteArray(0, length);
                Inspector.inspect(key, "ss_encrypt key");
            }
            @Override
            public void postCall(Emulator<?> emulator, RegisterContext ctx, HookEntryInfo info) {
                System.out.println("ss_encrypt.postCall R0=" + ctx.getLongArg(0));
            }
        });
        hookZz.disable_arm_arm64_b_branch();
        hookZz.instrument(module.base + 0x00000F5C + 1, new InstrumentCallback<Arm32RegisterContext>() {
            @Override
            public void dbiCall(Emulator<?> emulator, Arm32RegisterContext ctx, HookEntryInfo info) { // 通过base+offset inline wrap内部函数,在IDA看到为sub_xxx那些
                System.out.println("R3=" + ctx.getLongArg(3) + ", R10=0x" + Long.toHexString(ctx.getR10Long()));
            }
        });
 
        Dobby dobby = Dobby.getInstance(emulator);
        dobby.replace(module.findSymbolByName("ss_encrypted_size"), new ReplaceCallback() { // 使用Dobby inline hook导出函数
            @Override
            public HookStatus onCall(Emulator<?> emulator, HookContext context, long originFunction) {
                System.out.println("ss_encrypted_size.onCall arg0=" + context.getIntArg(0) + ", originFunction=0x" + Long.toHexString(originFunction));
                return HookStatus.RET(emulator, originFunction);
            }
            @Override
            public void postCall(Emulator<?> emulator, HookContext context) {
                System.out.println("ss_encrypted_size.postCall ret=" + context.getIntArg(0));
            }
        }, true);
 
        IxHook xHook = XHookImpl.getInstance(emulator); // 加载xHook,支持Import hook,文档看https://github.com/iqiyi/xHook
        xHook.register("libttEncrypt.so", "strlen", new ReplaceCallback() { // hook libttEncrypt.so的导入函数strlen
            @Override
            public HookStatus onCall(Emulator<?> emulator, HookContext context, long originFunction) {
                Pointer pointer = context.getPointerArg(0);
                String str = pointer.getString(0);
                System.out.println("strlen=" + str);
                context.push(str);
                return HookStatus.RET(emulator, originFunction);
            }
            @Override
            public void postCall(Emulator<?> emulator, HookContext context) {
                System.out.println("strlen=" + context.pop() + ", ret=" + context.getIntArg(0));
            }
        }, true);
        xHook.register("libttEncrypt.so", "memmove", new ReplaceCallback() {
            @Override
            public HookStatus onCall(Emulator<?> emulator, long originFunction) {
                RegisterContext context = emulator.getContext();
                Pointer dest = context.getPointerArg(0);
                Pointer src = context.getPointerArg(1);
                int length = context.getIntArg(2);
                Inspector.inspect(src.getByteArray(0, length), "memmove dest=" + dest);
                return HookStatus.RET(emulator, originFunction);
            }
        });
        xHook.register("libttEncrypt.so", "memcpy", new ReplaceCallback() {
            @Override
            public HookStatus onCall(Emulator<?> emulator, long originFunction) {
                RegisterContext context = emulator.getContext();
                Pointer dest = context.getPointerArg(0);
                Pointer src = context.getPointerArg(1);
                int length = context.getIntArg(2);
                Inspector.inspect(src.getByteArray(0, length), "memcpy dest=" + dest);
                return HookStatus.RET(emulator, originFunction);
            }
        });
        xHook.refresh(); // 使Import hook生效
    }

第三步: 添加调试及主动调用

1
2
3
4
5
6
7
    if (logging) {
        Debugger debugger = emulator.attach(DebuggerType.ANDROID_SERVER_V7); // 附加IDA android_server,可输入c命令取消附加继续运行
    }
    byte[] data = new byte[16];
    ByteArray array = TTEncryptUtils.callStaticJniMethodObject(emulator, "ttEncrypt([BI)[B", new ByteArray(vm, data), data.length); // 执行Jni方法
    return array.getValue();
}

第四步: 销毁环境

 

跟踪destroy

1
2
3
4
5
6
void destroy() throws IOException {
     emulator.close();
     if (logging) {
         System.out.println("destroy");
     }
 }

运行结果

在这里插入图片描述这个时候按c,继续,可以看到hook的结果以及JNI调用细节。
在这里插入图片描述

单步调试

ida_server的Debug方式相对简单,对于unidbg的强大之一在于它的单步调试-- Console Debugger

写在最后

作者的例子是以抖音作为例子的,还是很不错的。注释都写的比较清楚了。unidbg单步调试做的很棒,这个弥补了frida调试能力比较弱的缺点。

公众号

更多内容,欢迎关注我的微信公众号: 无情剑客。
在这里插入图片描述


第五届安全开发者峰会(SDC 2021)10月23日上海召开!限时2.5折门票(含自助午餐1份)

收藏
点赞1
打赏
分享
最新回复 (3)
雪    币: 207
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
System's 活跃值 2021-9-2 18:18
2
0
这个东西确实狠不错 ,我有个问题 假如native方法要传个Context 进去  。。。。那这个Context怎么生成怎么传
雪    币: 25
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
万里星河 活跃值 2021-9-3 08:27
3
0
这个东西确实狠不错 ,我有个问题 假如native方法会调用dex的函数 我如何把dex加载进去呢 如果这个dex函数还需要复杂的初始化 正如楼上所说 需要context啥的 又该如何生成呢 而且我按照unidbg github官方代码 一字不差照抄一遍加载我的so 总是提示一个错误 大概是说非法状态 需要setJni啥的 而且如果一个so如果动态注册了多个类的native函数 我需要在java层写这些类的模版吗?
雪    币: 20
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
江来一定会有 活跃值 2021-9-3 14:54
4
0
万里星河 这个东西确实狠不错 ,我有个问题 假如native方法会调用dex的函数 我如何把dex加载进去呢 如果这个dex函数还需要复杂的初始化 正如楼上所说 需要context啥的 又该如何生成呢 而且我按 ...
https://github.com/zhkl0228/unidbg/issues/5,把早期的issues和test案例看一遍就懂了
游客
登录 | 注册 方可回帖
返回