之前研究过某个 android app 的 vmp,通过模拟执行成功把里面的算法破解了。ios 版本的 vmp 一直没有破解,原因在于 vmp init 阶段符号找不到,我想排查问题,但海量的日志让我难以分析,所以就放弃模拟执行这条路了。app 本身有反调试,当时也没仔细去研究,所以就无法调试。由于不知道 vmp 解释器在哪, 我采用 ghidra 指令匹配收集了一堆可能是 instruction handler的点, 但frida hook指令很容易崩溃,且无法知道正确性。
而后的一段时间里,一直没碰这个算法,直到在看雪看到了 lldb-trace(https://bbs.pediy.com/thread-268633.htm), 它能够 dump 指令, 所以理论上,我只要把整个vmp 的流程 dump 下来就可以了。
0x02 收集信息
-
前面提到过,app 有反调试, 既然要用 lldb-trace, 那么 首先是要过反调试。
-
android 的 vmp 实现, 在 ios 里也有相似得实现, 所以,可以根据 android 定位到 ios 的部分关键函数
-
android 端的 bc 通过模拟执行,大概跑了200w 条指令,所以算法不复杂
-
android 端的 bc hash 算法部分只包含了
+-*/ << |&^
这几条指令,理论上,找到这几个handler,算法就能破解了 -
根据 android 的 bc, 可以知道 bc 是加了指令混淆的,但这个混淆效果不好,很容易被破解。
0x03 验证
反调试
首先是反调试, 之前想着反调试应该是不会弱, 所以也一直没去看。最近想着去调试一下, 断点放main 函数,看看会不会不行, IDA里可以发现main 函数里,开头就是ptrace。后来我又搜了一下ptrace字符串,发现大部分函数里塞了一个ptrace。
反反调试,比较简单, 网上抄一下反 ptrace 的代码,做成插件即可, 然后就可以反反调试了。
测试lldb-trace
测试的时候,我直接把 lldb-trace 的入口点设置在 oc 的函数入口, 然后开始跑,跑了之后发现指令dump 速度较慢, 而完整的算法指令数量在亿级别,所以直接trace 签名入口点不行。下图是trace的示例,其中w26
寄存器对应的是指令的序号,trace 结果符合预期。
ios vmp 定位及分析
参考android vmp ,从 VMP::Run
处开始跟, 由于 vmp 本身也被混淆了,所以这里也花了一定的时间去找对应的点。最后成功定位到了vmp 的解释器入口并尝试在入口点开始 lldb 调试。我发现从这里trace,指令数还是很大(百万-千万级别),还是得想办法优化。
算法本身会反射OC的方法获取数据, 根据 android 的 bc 算法, 数据越长,指令越多, 所以第一个操作是写插件,把数据长度改为固定一个字节, 这个操作直接缩短了执行指令数。第二, 优化lldb-trace, 看看实际需要分析的指令有多少, 这个我做了但效果感觉并不明显就放弃了。第三,既然算法是在获取上层数据后才开始的, 那么前面初始化的部分是不需要跟踪的,这个也能够节省很多指令。最后, 我侥幸定位到了真正的解释器入口,但我们仍需要确认下解释器的执行流程,方便我们后续操作。
从 ghidra 上看, interpreter
是解释器,输入是 text 地址及 pc 值,返回的是一下个pc 的地址, 所以这个函数是执行单条指令的,我们可以trace一下这个函数看看如何解析的。
一顿分析之后,大概可以确认,cmp w26, 0x1f 在比较操作码, 因此 hook 这里可以得到执行的操作码,不过这个用处不大, 我们的目的是找到 handler 的地址,可以肯定再往下就是handler的地址,但我没去分析,感觉单条分析比较费时间(我的目标是破解被保护的算法而非vmp)。后面的想法就是,根据结果往上推算术操作的handler了。
dump handler
前面提到过, interpreter
是单条指令的解释器,所以理论上,只要把这个函数所在的循环跑完,结果就有了。在实际跑的过程中,我发现,不管是lldb-trace, 还是 frida stralker 都会卡死在一个内存拷贝上,这让我有点摸不着头脑(猜测可能是因为frida 导致的),后来我放弃使用frida hook 就好了。因为我们只要dump 算法部分就行, 所以其他反射获取数据的操作我们是不需要管的, 这里还需要确认一下 bridge 在哪里,以及何时开始 dump。bridge可以理解为vmp 调用外部函数的一种方法,比如vmp想要调用memcpy, 就可以通过bridge调用。
经过一顿调试之后, 反射的调用结束了, 我们再次到达了解释器的入口点,这个时候就要开始进行 hash 算法了。
接着开始使用lldb-trace dump 指令流。刚开始,我是手动的,后来发现指令一共有300都条,手动操作不太行,就学了一下给断点加命令,让他自动化的把所有的指令跑完。我的预期是每一个 handler 都有一个 trace 文件对应,但后来,不知道咋回事,所有的指令都跑进一个trace文件了。不过好在最后找了一下运算结果, 发现在日志里面, 所以这个dump 算是成功了。
日志分析
这块就没什么技术含量了,我们知道vmp 输入和输出,就可以根据输出反向查找运算过程,重点关注跟结果相关算术指令。最终能够得到
0x101d5abe0 - 0x0000000100f94000 add
0x101d5adf8 sub
0x101d5c688 xor
0x101d59664 and
0x101d59664 and
0x101d594e8 orr
0x101d5a6bc lsl x8, x8, x24
根据日志已经能够将算法白盒化了, 但是由于日志找起来比较麻烦,所以找到handler 之后,还是使用frida 进行操作来得快。
白盒化
frida hook 上述指令之后, 真机上跑一下就有结果了。这里有个坑是,frida 脚本写的时候一定要对好寄存器,操作码,一旦写错一个,理解上可能就会有偏差了,排查问题会比较麻烦。最后dump 出来:
sub 0 - 0= 0
and 0x5a & 0x0= 0x0
orr 0x0 | 0x5a= 0x5a
add 0 + 5a= 5a
add 1e04bd18 + 5a= 1e04bd72
sub 61fb42e7 - 5a= 61fb428d
and 0x494afa3c & 0x61fb428d= 0x414a420c
and 0xb6b50800 & 0x1e04bd72= 0x16040800
orr 0x16040542 | 0x414a420c= 0x574e474e
xor 0x494afa3c ^ 0x574e474e= 0x1e04bd72
add 2c57bea6 + fffffffe= 2c57bea4
and 0x1e04bd72 & 0x1e04bd72= 0x1e04bd72
and 0x4296327e & 0x2c57bea4= 0x163224
sub d3a84000 - fffffffe= d3a84002
and 0xbd69d000 & 0xd3a84000= 0x91284000
orr 0x91284000 | 0x163224= 0x913e7224
xor 0xbd69d000 ^ 0x913e7000= 0x2c57a000
add 2c57bea4 + 1e04bd72= 4a5c7c16
sub 4a5c7bbd - 4a5c7c16= ffffffa7
and 0x7b25c341 & 0x0= 0x0
add b5a38442 + 4a5c7c16= 58
and 0x84da4000 & 0x58= 0x0
orr 0x18 | 0x7b25c301= 0x7b25c319
xor 0x7b25c341 ^ 0x7b25c319= 0x58
lsl 0x58 << 0x5= 0xb00
sub b00 - 58= aa8
and 0x5a & 0xaa8= 0x8
orr 0xaa8 | 0x5a= 0xafa
add 8 + afa= b02
add 1e04bd18 + b02= 1e04c81a
sub 61fb42e7 - b02= 61fb37e5
and 0x494afa3c & 0x61fb37e5= 0x414a3224
and 0xb6b50800 & 0x1e04c81a= 0x16040800
orr 0x16040002 | 0x414a3224= 0x574e3226
xor 0x494afa3c ^ 0x574e3226= 0x1e04c81a
add 2c57bea6 + ffffffff= 2c57bea5
and 0x1e04c81a & 0x1e04c81a= 0x1e04c81a
and 0x4296327e & 0x2c57bea5= 0x163224
sub d3a84000 - ffffffff= d3a84001
and 0xbd69d000 & 0xd3a84000= 0x91284000
orr 0x91284000 | 0x163224= 0x913e7224
xor 0xbd69d000 ^ 0x913e7000= 0x2c57a000
add 2c57bea5 + 1e04c81a= 4a5c86bf
sub 4a5c7bbd - 4a5c86bf= fffff4fe
and 0x7b25c341 & 0xfffff800= 0x7b25c000
add b5a38442 + 4a5c86bf= b01
and 0x84da4000 & 0xb01= 0x0
orr 0x800 | 0x7b25c040= 0x7b25c840
xor 0x7b25c341 ^ 0x7b25c840= 0xb01
lsl 0x0 << 0x0= 0x0
lsl 0x0 << 0x0= 0x0
orr 0x0 | 0x89393800= 0x89393800
orr 0x0 | 0x76c6c6f8= 0x76c6c6f8
xor 0x89393800 ^ 0x0= 0x89393800
sub 0 - 76c6c6f8= 89393908
add 76c6c6f9 + 89393907= 0
lsl 0x0 << 0x0= 0x0
lsl 0x0 << 0x0= 0x0
lsl 0x0 << 0x0= 0x0
lsl 0xb01 << 0x5= 0x16020
sub 16020 - b01= 1551f
and 0x5a & 0x1551f= 0x1a
orr 0x1551f | 0x5a= 0x1555f
add 1a + 1555f= 15579
add 1e04bd18 + 15579= 1e061291
sub 61fb42e7 - 15579= 61f9ed6e
and 0x494afa3c & 0x61f9ed6e= 0x4148e82c
and 0xb6b50800 & 0x1e061291= 0x16040000
orr 0x16040081 | 0x4148e82c= 0x574ce8ad
xor 0x494afa3c ^ 0x574ce8ad= 0x1e061291
add 2c57bea6 + 0= 2c57bea6
and 0x1e061291 & 0x1e061291= 0x1e061291
and 0x4296327e & 0x2c57bea6= 0x163226
sub d3a84000 - 0= d3a84000
and 0xbd69d000 & 0xd3a84000= 0x91284000
orr 0x91284000 | 0x163226= 0x913e7226
xor 0xbd69d000 ^ 0x913e7000= 0x2c57a000
add 2c57bea6 + 1e061291= 4a5dd137
sub 4a5c7bbd - 4a5dd137= fffeaa86
and 0x7b25c341 & 0xfffea800= 0x7b248000
add b5a38442 + 4a5dd137= 15579
and 0x84da4000 & 0x15579= 0x4000
orr 0x1438 | 0x7b248200= 0x7b249638
xor 0x7b25c341 ^ 0x7b249638= 0x15579
lsl 0x0 << 0x0= 0x0
lsl 0x0 << 0x0= 0x0
orr 0x0 | 0x89393800= 0x89393800
orr 0x0 | 0x76c6c6f8= 0x76c6c6f8
xor 0x89393800 ^ 0x76c6c6f8= 0xfffffef8
sub 89393800 - ffffffff= 89393801
add 76c6c6f9 + 89393908= 1
lsl 0x0 << 0x0= 0x0
lsl 0x0 << 0x0= 0x0
lsl 0x0 << 0x0= 0x0
lsl 0x15579 << 0x5= 0x2aaf20
sub 2aaf20 - 15579= 2959a7
and 0x5a & 0x2959a7= 0x2
orr 0x2959a7 | 0x5a= 0x2959ff
add 2 + 2959ff= 295a01
add 1e04bd18 + 295a01= 1e2e1719
sub 61fb42e7 - 295a01= 61d1e8e6
and 0x494afa3c & 0x61d1e8e6= 0x4140e824
and 0xb6b50800 & 0x1e2e1719= 0x16240000
orr 0x16240501 | 0x4140e824= 0x5764ed25
xor 0x494afa3c ^ 0x5764ed25= 0x1e2e1719
add 2c57bea6 + 1= 2c57bea7
and 0x1e2e1719 & 0x1e2e1719= 0x1e2e1719
and 0x4296327e & 0x2c57bea7= 0x163226
sub d3a84000 - 1= d3a83fff
and 0xbd69d000 & 0xd3a84000= 0x91284000
orr 0x91284000 | 0x163226= 0x913e7226
xor 0xbd69d000 ^ 0x913e7000= 0x2c57a000
add 2c57bea7 + 1e2e1719= 4a85d5c0
sub 4a5c7bbd - 4a85d5c0= ffd6a5fd
and 0x7b25c341 & 0xffd6a800= 0x7b048000
add b5a38442 + 4a85d5c0= 295a02
and 0x84da4000 & 0x295a02= 0x84000
orr 0x81802 | 0x7b048141= 0x7b0c9943
xor 0x7b25c341 ^ 0x7b0c9943= 0x295a02
lsl 0x0 << 0x0= 0x0
lsl 0x0 << 0x0= 0x0
orr 0x1 | 0x89393800= 0x89393801
orr 0x1 | 0x76c6c6f8= 0x76c6c6f9
xor 0x89393800 ^ 0x76c6c6f9= 0xfffffef9
sub 89393800 - fffffffe= 89393802
add 76c6c6f9 + 89393909= 2
lsl 0x0 << 0x0= 0x0
lsl 0x0 << 0x0= 0x0
lsl 0x0 << 0x0= 0x0
lsl 0x295a02 << 0x5= 0x52b4040
sub 52b4040 - 295a02= 501e63e
and 0x5a & 0x501e63e= 0x1a
orr 0x501e63e | 0x5a= 0x501e67e
add 1a + 501e67e= 501e698
add 1e04bd18 + 501e698= 2306a3b0
sub 61fb42e7 - 501e698= 5cf95c4f
and 0x494afa3c & 0x5cf95c4f= 0x4848580c
and 0xb6b50800 & 0x2306a3b0= 0x22040000
orr 0x22040180 | 0x4848580c= 0x6a4c598c
xor 0x494afa3c ^ 0x6a4c598c= 0x2306a3b0
add 2c57bea6 + 2= 2c57bea8
and 0x2306a3b0 & 0x2306a3b0= 0x2306a3b0
and 0x4296327e & 0x2c57bea8= 0x163228
sub d3a84000 - 2= d3a83ffe
and 0xbd69d000 & 0xd3a84000= 0x91284000
orr 0x91284000 | 0x163228= 0x913e7228
xor 0xbd69d000 ^ 0x913e7000= 0x2c57a000
add 2c57bea8 + 2306a3b0= 4f5e6258
sub 4a5c7bbd - 4f5e6258= fafe1965
and 0x7b25c341 & 0xfafe1800= 0x7a240000
add b5a38442 + 4f5e6258= 501e69a
and 0x84da4000 & 0x501e69a= 0x4004000
orr 0x400249a | 0x7a240141= 0x7e2425db
xor 0x7b25c341 ^ 0x7e2425db= 0x501e69a
lsl 0x0 << 0x0= 0x0
lsl 0x0 << 0x0= 0x0
orr 0x2 | 0x89393800= 0x89393802
orr 0x2 | 0x76c6c6f8= 0x76c6c6fa
xor 0x89393800 ^ 0x76c6c6fa= 0xfffffefa
sub 89393800 - fffffffd= 89393803
add 76c6c6f9 + 8939390a= 3
lsl 0x0 << 0x0= 0x0
lsl 0x0 << 0x0= 0x0
add 12341234 + 501e69a= 1735f8ce
orr 0x6d99e180 | 0x0= 0x6d99e180
orr 0x6d99e180 | 0x0= 0x6d99e180
lsl 0x0 << 0x0= 0x0
输入是 Z
, 输出是1735f8ce
, 根据 android 的白盒代码,大改也能把流程猜出一二了。需要注意的是,这里有指令混淆,很多指令其实根本用不到。
优化
上述运算过程还需通过人工分析,耗时仍比较多,那么有没有可能通过一些工具自动化把核心的几个运算提取出来呢?这个我没尝试,但我觉得是比较有意思的想法。
0x04 总结
目前移动端 vmp 还是以取码-解码-解释执行
这种方式为多,其缺点也比较明显,一旦攻击者找到解释执行的入口点,就可以使用各种trace工具把执行日志dump 下来进行离线分析,再配合各种工具,破解会变得相对容易一些。其次,被 vmp 保护的算法,需要有一定的复杂度,如果用一些标准算法或是简单的古典算法,很容易被分析出来,可能都不需要分析vmp了,这几个特征在我分析的 vmp 中都有遇到过。
github上开源了一个VMProtect-devirtualization(https://github.com/JonathanSalwan/VMProtect-devirtualization), 作者利用符号执行以及llvm 优化,完成了对 vmp 的破解,这也给我们提供了一个分析的方向。
0x05 参考
-
vmp_de(https://github.com/JonathanSalwan/VMProtect-devirtualization)
-
vmp入门(https://www.52pojie.cn/thread-713219-1-1.html)
原文始发于微信公众号(RayTracing):记录一次iOS VMP逆向分析过程