背景
之前研究过某个 android app 的 vmp,通过模拟执行成功把里面的算法破解了。 ios 版本的 vmp 一直没有破解,原因在于 vmp init 阶段符号找不到,我想排查问题,但海量的日志让我难以分析,所以就放弃模拟执行这条路了。app 本身有反调试,当时也没仔细去研究,所以就无法调试。 由于不知道 vmp 解释器在哪, 我采用 ghidra 指令匹配收集了一堆可能是 instruction handler的点, 但frida hook指令很容易崩溃,且无法知道正确性。
而后的一段时间里,一直没碰这个算法,直到在看雪看到了 lldb-trace, 它能够 dump 指令, 所以理论上,我只要把整个vmp 的流程 dump 下来就可以了。
收集信息
- 前面提到过,app 有反调试, 既然要用 lldb-trace, 那么 首先是要过反调试。
- android 的 vmp 实现, 在 ios 里也有相似得实现, 所以,可以根据 android 定位到 ios 的部分关键函数
- android 端的 bc 通过模拟执行,大概跑了200w 条指令,所以算法不复杂
- android 端的 bc hash 算法部分只包含了
+-*/ << |&^
这几条指令,理论上,找到这几个handler,算法就能破解了
- 根据 android 的 bc, 可以知道 bc 是加了指令混淆的,但这个混淆效果不好,很容易被破解。
验证
反调试
首先是反调试, 之前想着反调试应该是不会弱, 所以也一直没去看。最近想着去调试一下, 断点放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 输入和输出,就可以根据输出反向查找运算过程,重点关注跟结果相关算术指令。最终能够得到
1 2 3 4 5 6 7
| 0x101d5abe0 - 0x0000000100f94000 add 0x101d5adf8 sub 0x101d5c688 xor 0x101d59664 and 0x101d59664 and 0x101d594e8 orr 0x101d5a6bc lsl x8, x8, x24
|
根据日志已经能够将算法白盒化了, 但是由于日志找起来比较麻烦,所以找到handler 之后,还是使用frida 进行操作来得快。
白盒化
frida hook 上述指令之后, 真机上跑一下就有结果了。 这里有个坑是,frida 脚本写的时候一定要对好寄存器,操作码,一旦写错一个,理解上可能就会有偏差了,排查问题会比较麻烦。最后dump 出来:
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 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167
| 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 的白盒代码,大改也能把流程猜出一二了。需要注意的是,这里有指令混淆,很多指令其实根本用不到。
优化
上述运算过程还需通过人工分析,耗时仍比较多,那么有没有可能通过一些工具自动化把核心的几个运算提取出来呢?这个我没尝试,但我觉得是比较有意思的想法。
总结
目前移动端 vmp 还是以取码-解码-解释执行
这种方式为多,其缺点也比较明显,一旦攻击者找到解释执行的入口点,就可以使用各种trace工具把执行日志dump 下来进行离线分析,再配合各种工具,破解会变得相对容易一些。其次,被 vmp 保护的算法,需要有一定的复杂度,如果用一些标准算法或是简单的古典算法,很容易被分析出来,可能都不需要分析vmp了,这几个特征在我分析的 vmp 中都有遇到过。
github上开源了一个VMProtect-devirtualization, 作者利用符号执行以及llvm 优化,完成了对 vmp 的破解,这也给我们提供了一个分析的方向。
参考