记录一次ios vmp 分析

背景

之前研究过某个 android app 的 vmp,通过模拟执行成功把里面的算法破解了。 ios 版本的 vmp 一直没有破解,原因在于 vmp init 阶段符号找不到,我想排查问题,但海量的日志让我难以分析,所以就放弃模拟执行这条路了。app 本身有反调试,当时也没仔细去研究,所以就无法调试。 由于不知道 vmp 解释器在哪, 我采用 ghidra 指令匹配收集了一堆可能是 instruction handler的点, 但frida hook指令很容易崩溃,且无法知道正确性。

而后的一段时间里,一直没碰这个算法,直到在看雪看到了 lldb-trace, 它能够 dump 指令, 所以理论上,我只要把整个vmp 的流程 dump 下来就可以了。

收集信息

  1. 前面提到过,app 有反调试, 既然要用 lldb-trace, 那么 首先是要过反调试。
  2. android 的 vmp 实现, 在 ios 里也有相似得实现, 所以,可以根据 android 定位到 ios 的部分关键函数
  3. android 端的 bc 通过模拟执行,大概跑了200w 条指令,所以算法不复杂
  4. android 端的 bc hash 算法部分只包含了 +-*/ << |&^ 这几条指令,理论上,找到这几个handler,算法就能破解了
  5. 根据 android 的 bc, 可以知道 bc 是加了指令混淆的,但这个混淆效果不好,很容易被破解。

验证

反调试

首先是反调试, 之前想着反调试应该是不会弱, 所以也一直没去看。最近想着去调试一下, 断点放main 函数,看看会不会不行, IDA里可以发现main 函数里,开头就是ptrace。后来我又搜了一下ptrace字符串,发现大部分函数里塞了一个ptrace。
WechatIMG38.jpeg
WechatIMG37.jpeg

反反调试 比较简单, 网上抄一下反 ptrace 的代码,做成插件即可, 然后就可以反反调试了。

测试lldb-trace

测试的时候,我直接把 lldb-trace 的入口点设置在 oc 的函数入口, 然后开始跑,跑了之后发现指令dump 速度较慢, 而完整的算法指令数量在亿级别,所以直接trace 签名入口点不行。下图是trace的示例,其中w26寄存器对应的是指令的序号,trace 结果符合预期。
WechatIMG40.jpeg

ios vmp 定位及分析

参考android vmp ,从 VMP::Run 处开始跟, 由于 vmp 本身也被混淆了,所以这里也花了一定的时间去找对应的点。最后成功定位到了vmp 的解释器入口并尝试在入口点开始 lldb 调试。我发现从这里trace,指令数还是很大(百万-千万级别),还是得想办法优化。

算法本身会反射OC的方法获取数据, 根据 android 的 bc 算法, 数据越长,指令越多, 所以第一个操作是写插件,把数据长度改为固定一个字节, 这个操作直接缩短了执行指令数。 第二, 优化lldb-trace, 看看实际需要分析的指令有多少, 这个我做了但效果感觉并不明显就放弃了。 第三,既然算法是在获取上层数据后才开始的, 那么前面初始化的部分是不需要跟踪的,这个也能够节省很多指令。 最后, 我侥幸定位到了真正的解释器入口,但我们仍需要确认下解释器的执行流程,方便我们后续操作。

WechatIMG39.jpeg

从 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 的破解,这也给我们提供了一个分析的方向。

参考