首页
社区
课程
招聘
[原创]魔改unidbg:极致的trace速度提升
发表于: 2天前 1545

[原创]魔改unidbg:极致的trace速度提升

2天前
1545

unidbgtrace 速度太慢了?别急,我来助你

我们这一篇文章主要是来改进一下 unidbg 的自带 trace 功能,对于这一功能,我的评价是目前的 trace 能用但是并不好用;

先说一下速度吧,实测比原版unidbg的快几十倍,然后混淆恶心的ollvm+vmp可以达到一百多倍,也就是需要几小时的可以i直接变成几分钟。

找到我的魔改unidbg 8a2K9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6Y4K9i4c8Z5N6h3u0Q4x3X3g2U0L8$3#2Q4x3V1k6x3N6h3&6r3k6h3&6Y4b7$3S2W2L8W2)9J5c8Y4g2F1K9h3c8T1k6H3`.`. 然后 git clone,之后正常使用就行。

然后这里我是提供了类似 traceCode 的 API,叫做 traceCodeText,因为我后续会支持一波二进制格式的 trace,那个速度更快,不过需要和桌面端配合使用;

注意:我保留了原版的 traceCode,这样不影响你项目迁移后的编译问题;

会用了对吧,对优化理论不感兴趣的话,那就可以直接跳到最后了。

这期文章我们就来解决这个问题,并且试图给大家讲明白我都做了哪些优化;

UnidbgTrace 功能本质是一个 逐指令回调钩子CodeHook):Unicorn(或 Dynarmic/Hypervisor)引擎在执行每一条 ARM/ARM64 指令前,都会回调一次 Java 层的 hook() 方法。在这个回调里,原版引擎需要做:

对于正常的 SO,一次函数调用可能产生几百到几万条 Traceunidbg 原版这套流程完全够用。但 OLLVMVMP 保护会让函数的执行路径急剧膨胀——OLLVM 的状态机调度让每个基本块的跳转都要经过 dispatcher 中转,VMP 更是把每条原始指令翻译成数十条 VM Handler 指令逐条解释执行:

unidbg 做这一步其实是没问题的,主要是为了从机器码到汇编指令,这样可以知道哪些寄存器发生了变化,以便于后续读取变化的寄存器打印。

"stp x29, x30, [sp, #-0x20]!" 这一条指令就读取了 sp 寄存器的值,然后偏移地址,然后读 x29x30 的值,然后把值写入到 sp-0x20 的地址处;unidbg 通过 Capstone 可以得知,read 的寄存器有 spx29x30write 的寄存器有 sp

但是这一步对于 OLLVM/VMPtrace 来说,会有很大的性能浪费,因为 OLLVM/VMP 的指令膨胀效果,其实存在着大量的重复指令,这些重复指令的机器码都是一样的;此时如果依旧每一条指令都调用一次 Capstonedisassemble(),那么就会非常慢。

如何解决这个问题:很简单,直接上缓存,你 OLLVM/VMP 再强,能有多少不同的指令?

这里我直接使用了 L1/L2 两级缓存,这里为啥不只用一级缓存?比如为什么不直接用一个 HashMap<地址, 反汇编结果> 来搞定?

因为:这在 OLLVM/VMP 场景下效率不是最优的,原因是 HashMap 每次查找需要:

如果是上亿级别的指令,上亿次 × 这套流程 = 一笔可观的开销。

所以这里我们再搞一级缓存,用数组来代替,直接使用地址取模来定位,这样可以省去 HashMap 的开销。

ok 接下来拿 VMP 举个例子;这里假设 VMP 的 VM Handler 循环执行 3 条指令,地址分别是 0x10000x10040x1008,循环 N 次:

第 1 轮(全部 cache miss)

第 2 轮(全部命中)

第 3 轮到 一亿轮:全部 L1 hit,Capstone 调用次数 = 0。

举个冲突的例子。假设 OLLVM 有两个 Basic Block,地址恰好冲突(算出来的 L1 格子编号一样):

执行过程:

第 1 步:执行 Block_A0x00001000

第 2 步:跳到 Block_B,执行 0x02001000

第 3 步:跳回 Block_A,又执行 0x00001000

如果没有 L2,第 3 步就要重新调 CapstoneOLLVM 的 Block 之间来回跳转,没有 L2 的话就会反复 miss 反复调 CapstoneL2 就是干这个的——接住被 L1 挤掉的条目。

实测上了这个方案,速度大幅提升;

如果 SO 运行时,同一个地址的机器码发生变化,缓存的反汇编结果就会失效。解决方案是读取当前机器码并与缓存中的做比较:

实测这个方案,只会带来一部分性能损耗,对我个人而言是可以接受的;目前还没想到比较好的方案,主要是不动 Unicorn 的话……

其实,对于确认没有 SMC 的场景(绝大多数情况),可以通过 setDisableSMC(true) 跳过这个检查,这样连 mem_read 都省掉。

--- 

在解决了 Capstone 重复调用后,性能 Profiling 显示新的热点转移到了 Java 对象分配。上亿次回调中,以下操作产生了海量临时对象:

1. SMC 检测中的 ByteBuffer

上亿次 × ByteBuffer.allocate() = 海量堆内存分配 → Young GC 频繁触发。

2. findModuleByAddress 模块查表

String.format 本身也会创建 FormatterStringBuilderchar 数组等大量临时对象。

3. Hexdump 格式化

1. 位运算替代 ByteBuffer

2. 预计算前缀(Precomputed Prefix)

findModuleByAddress 和字符串格式化从热循环移到缓存构建时。每个 CachedInstruction 在首次创建时就固化好完整的前缀文本:

同样的思路也应用到了寄存器映射:Capstoneins.mapToUnicornReg()ins.regName() 调用都被预计算并存入 HashMap,避免在热循环中重复通过 JNI 调用 Capstone

3. HEX 静态速查替代 String.format

这个短板不容易被想到,但实际上作用很明显,之所以不在前面讲,是因为这个其实太基础了,本来都不打算提。

经过前两步优化后,瓶颈转移到了 I/O 写入。原版的写入方式是:

PrintStream.println() 内部有 synchronized 锁。上亿次同步写入意味着上亿次锁获取/释放。而且文件 I/O 本身就有系统调用开销。

代码如下:

此外,我们还加了函数级别的跟踪,后面会提到;其中,函数调用日志(Call/Ret 的分隔线)也被重定向到 asyncBlockBuffer。原版中 Call/Ret 的输出是通过独立的 PrintStream 直接写的,在多线程批处理模式下,会导致函数调用链与底层汇编指令的时序错乱。新版通过 queueOut(一个伪 PrintStream,实际追加到 asyncBlockBuffer)统一了所有输出的序列化:

在性能问题解决后,顺便大幅增强了 Trace 的语义可读性吧。加了这些功能实际上会拖慢速度,但是由于我们速度很快了,所以不差这点了,要更多侧重分析方便性了;

引入了 TraceCallParser 接口和四个默认实现,这样大家也可以自己改格式:

这一点大家仔细 trace 体验吧,这东西讲了也没啥价值,就是如何通过 PLT/GOT 表获取符号的事情,其中 ARM32 和 ARM64 的实现方式还不太一样,本来都打算懒得支持 ARM32 了,但是想了一下既然要开源,就都做好吧;

返回值中的 jclass/jmethodID 会被缓存到 jniIdMap,后续使用这些 ID 的调用会自动附上对应的类名/方法签名。

针对 ld/st 指令操作的绝对地址,进行内存 hexdump 打印,这样分析时可以根据值最早出现去追踪,相当于就是类似龙哥 DataSearch 的插件吧,这里除了 hexdump 我还贴心的打印了大端序,因为这是个实际使用的小痛点,哈哈哈哈。

经过以上优化,Java 层已经基本不再是瓶颈了。但仍然存在一些改进空间,这里分析一下:

无论 Java 层多快,每条指令都要从 C/C++ 引擎穿越到 Java 层这件事本身就有不可消除的开销:

每条指令的回调中,还需要读取寄存器的当前值(用于显示变化)。每次 backend.reg_read() 也是一次 JNI 调用。一条 add x0, x1, x2 就需要读 3 个寄存器,一亿条指令 × 平均 3 次 reg_read = 3 亿次 JNI 调用

可能的解决方向:

上亿行 Trace,由于写入的字节过多,即使用了异步批量写入,磁盘的顺序写入带宽仍然是一个物理上限。

可能的解决方向:

Unicorn 是基于 QEMU 的翻译执行引擎,每条 ARM 指令需要先翻译成 TCG IR,再翻译成宿主机指令。这个翻译 + 执行的过程本身就比原生执行慢 10~100 倍。如果后续能解决这个问题,或者在宿主机上随便跑arm指令,那么我个人觉得速度就不再是问题了;


传播安全知识、拓宽行业人脉——看雪讲师团队等你加入!

最后于 2天前 被x1a0f3n9编辑 ,原因:
收藏
免费 102
支持
分享
最新回复 (61)
雪    币: 8566
活跃值: (5243)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
2
666
2天前
0
雪    币: 3687
活跃值: (8748)
能力值: ( LV7,RANK:102 )
在线值:
发帖
回帖
粉丝
3
看看大手子是怎么追踪的
2天前
0
雪    币: 200
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
4
tql
2天前
0
雪    币: 3
活跃值: (283)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
5
666
2天前
0
雪    币: 1838
活跃值: (1790)
能力值: ( LV4,RANK:40 )
在线值:
发帖
回帖
粉丝
6
tql
2天前
0
雪    币: 7
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
7
向大佬学习
2天前
0
雪    币: 150
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
8
666
2天前
0
雪    币: 211
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
9
666
2天前
0
雪    币: 3
活跃值: (1128)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
10
666
2天前
0
雪    币: 0
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
11
666
2天前
0
雪    币: 6
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
12
感谢分享
2天前
0
雪    币: 20
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
13
666
2天前
0
雪    币: 20
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
14
tql
2天前
0
雪    币: 201
活跃值: (2112)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
15
66666
2天前
0
雪    币: 2790
活跃值: (6016)
能力值: ( LV6,RANK:90 )
在线值:
发帖
回帖
粉丝
16
感谢分享
1天前
0
雪    币: 23
活跃值: (1740)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
17
666
1天前
0
雪    币: 3851
活跃值: (4656)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
18
fjqisba 看看大手子是怎么追踪的
啊?是大手子啊?
1天前
0
雪    币: 375
活跃值: (3541)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
19
666
1天前
0
雪    币: 666
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
20
Thanks
1天前
0
雪    币: 3985
活跃值: (6392)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
21
感谢分享
1天前
0
雪    币: 299
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
22
学习学习
1天前
0
雪    币: 4767
活跃值: (3239)
能力值: ( LV7,RANK:140 )
在线值:
发帖
回帖
粉丝
23
tql
1天前
0
雪    币: 639
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
24
tql
1天前
0
雪    币: 2894
活跃值: (3229)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
25
tql
1天前
0
游客
登录 | 注册 方可回帖
返回