前言
vm 对我也说一直是个很有意思的东西,自想还原vmp失败以来,一有机会了解vm都会去看一下,js的vm有很多教程了,对我启发很大,这篇文章的实现方案是我的一种对vm还原的尝试,按照我最开始的想法是要还原到binja的 il 然后去构建一个函数的,但是我懒癌犯了,就不打算继续了(这个绝对是可以这么干的)
由于我懒得搭建 unidbg 去模拟执行(菜是原罪),所以本文全程使用静态分析去完成
此文更多的是解释我的vm还原的代码,以及对还原vm的一种尝试,而不是具体的介绍我代码是怎么写的(如果有需要的话,我可能会出一个视频来讲解怎么实现的代码,打字太累了),我的还原思路应该是可以通用还原别的vm的,我设计的时候也是尽可能的这么做的
阅读此文的前置条件:
1: 对参考文章中内容有一定了解
2: 对angr(符号执行)有一定了解
参考文章
某短视频虚拟机分析和还原 https://bbs.kanxue.com/thread-282300.htm
主要思路
我是这么划分 vm 解释器的(应改看不清 该函数偏移为 0x2c18)
本文解释我还原的代码主要围绕调用外部函数
错误的探索
在我上一篇还原 ollvm 的文章中,我还原的主要思路 是用 angr在分发器设置 负责跳转的寄存器的值,然后angr就可以把这个值对应的执行的过程全部记录下来,一直到下一次重新跳转到分发器,这次分析vm我最开始也是这个思路的,但是后面发现了问题,在ollvm中,我只是为了还原控制流,所以跟代码实际执行逻辑是不相关的,但 vm就不一样了,vm执行的过程是跟前面变量有关的,就导致无法像上次一样去还原
稍微解释下
1 2 3 4 5 6 7 | opcode 执行逻辑0xdfbdfc16 vreg0=30xdfbdfc150xdfbdfc140xdfbdfc13 if (vreg0 > 1) ip+=80xdfbdfc12 vreg0+=20xdfbdfc11 vreg0+=1 |
这里 opcode 0xdfbdfc13 对应的就是判断虚拟寄存器0的值大于1走一个流程,如果我 使用之前的方法,从分发器设置 opcode的值,然后一直符号执行直到下一次到分发器,就会出现分支(可能会很多),也有的时候,后面的opcode 会使用前面设置的虚拟寄存器,在符号执行后面的opcode的时候,不知道这个值.也会产生分支
正确思路
为了尽可能的减少分支,让符号执行的执行流尽可能的去接近 opcodes 对应的流程,而不包含解释器执行过程产生的分支,就需要用一个state(里面包含内存信息),从解释器开始执行一直到解释完所有opcode
还原出来的伪代码:
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 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 | sp = sp + -0x210sp + 0x208 = vreg_31sp + 0x200 = vreg_30sp + 0x1f8 = vreg_23sp + 0x1f0 = vreg_22sp + 0x1e8 = vreg_21sp + 0x1e0 = vreg_20sp + 0x1d8 = vreg_19sp + 0x1d0 = vreg_18sp + 0x1c8 = vreg_17sp + 0x1c0 = vreg_16vreg_16 = x0 | vreg_7vreg_11 = x4 + 0x10vreg_12 = x4 + 0x0vreg_2 = vreg_6 + 0x0x3 = vreg_6 + 0x8vreg_5 = vreg_6 + 0x10vreg_7 = vreg_6 + 0x18vreg_8 = vreg_6 + 0x20vreg_9 = vreg_6 + 0x28vreg_18 = x4 + 0x18vreg_10 = vreg_6 + 0x30vreg_6 = vreg_6 + 0x38vreg_19 = x4 + 0x8x1 = vreg_19 + 0x76sp + 0x88 = x1x4 = vreg_18 + 0x0branch0(先不处理)未知x1 = 0xffffffffff220000x1 = x1 | (0x5870 & 0xffff)x4 = x1 + vreg_6sp + 0x40 = x4x4 = x1 + vreg_10sp + 0x38 = x4x4 = x1 + vreg_9sp + 0x30 = x4x4 = x1 + vreg_8sp + 0x20 = x4x4 = x1 + vreg_7sp + 0x18 = x4x4 = x1 + vreg_5sp + 0x0 = x4vreg_23 = x1 + x3vreg_30 = x1 + vreg_2vreg_20 = x0 + 0x20sp + 0x1b0 = vreg_20vreg_5 = sp + 0x1b0x4 = x0 | vreg_30vreg_25 = x0 | vreg_16sp + 0x28 = vreg_11vreg_31 = ip+8sp + 0x8 = vreg_12call 0x2b40sp + 0x1a8 = vreg_20vreg_21 = sp + 0x1b8sp + 0x1a0 = vreg_21vreg_5 = sp + 0x1a0vreg_25 = x0 | vreg_16vreg_31 = ip+8x4 = x0 | vreg_23call 0x2b5cvreg_5 = sp + 0x190vreg_22 = x0 + 0x10sp + 0x190 = vreg_22vreg_25 = x0 | vreg_16vreg_31 = ip+8x4 = x0 | vreg_30call 0x2b40vreg_5 = sp + 0x180sp + 0x180 = vreg_22vreg_17 = sp + 0x198sp + 0x10 = vreg_17vreg_25 = x0 | vreg_16vreg_31 = ip+8x4 = x0 | vreg_30call 0x2b40sp + 0x150 = vreg_21sp + 0x158 = vreg_20sp + 0x160 = vreg_17sp + 0x168 = vreg_22sp + 0x178 = vreg_22vreg_20 = sp + 0x188sp + 0x170 = vreg_20x4 = sp + 0x0vreg_25 = x0 | vreg_16vreg_31 = ip+8vreg_5 = sp + 0x150call 0x2b94vreg_17 = vreg_19 + 0x40vreg_5 = sp + 0x140sp + 0x140 = vreg_17vreg_25 = x0 | vreg_16vreg_31 = ip+8x4 = x0 | vreg_30call 0x2b40vreg_30 = sp + 0x48vreg_22 = sp + 0x148sp + 0x130 = vreg_19vreg_23 = sp + 0x8sp + 0x128 = vreg_23sp + 0x138 = vreg_30x4 = sp + 0x18vreg_25 = x0 | vreg_16vreg_31 = ip+8vreg_5 = sp + 0x128call 0x2ba8vreg_5 = sp + 0x110x1 = x0 + 0x40sp + 0x118 = vreg_30sp + 0x110 = vreg_22sp + 0x120 = x1vreg_30 = sp + 0x20vreg_25 = x0 | vreg_16vreg_31 = ip+8x4 = x0 | vreg_30call 0x2bb8vreg_5 = sp + 0xf8x1 = vreg_22 + 0x40sp + 0x100 = vreg_23sp + 0xf8 = x1sp + 0x108 = vreg_19vreg_25 = x0 | vreg_16vreg_31 = ip+8x4 = x0 | vreg_30call 0x2bb8x1 = x0 + 32sp + 0xe8 = vreg_21vreg_19 = sp + 0x28sp + 0xe0 = vreg_19sp + 0xf0 = x1x4 = sp + 0x30vreg_25 = x0 | vreg_16vreg_31 = ip+8vreg_5 = sp + 0xe0call 0x2bc8x1 = vreg_19 + 0x26vreg_19 = sp + 0x10vreg_2 = x0 + 16x3 = sp + 0x88sp + 0xa8 = vreg_19sp + 0xb0 = vreg_2sp + 0xb8 = vreg_20sp + 0xc0 = vreg_22sp + 0xc8 = vreg_17sp + 0xd0 = x1sp + 0xd8 = x3x1 = sp + 0x88x1 = x1 + -0x26sp + 0x88 = x1x4 = sp + 0x38vreg_25 = x0 | vreg_16vreg_31 = ip+8vreg_5 = sp + 0xa8call 0x2be8vreg_2 = sp + 0x88未知x1 = vreg_2 + 0x26vreg_18 + 0x0 = x1sp + 0xa0 = vreg_21vreg_5 = sp + 0xa0vreg_17 = sp + 0x40vreg_25 = x0 | vreg_16vreg_31 = ip+8x4 = x0 | vreg_17call 0x2c04sp + 0x98 = vreg_19vreg_5 = sp + 0x98vreg_25 = x0 | vreg_16vreg_31 = ip+8x4 = x0 | vreg_17call 0x2c04vreg_5 = sp + 0x90sp + 0x90 = vreg_22vreg_25 = x0 | vreg_16vreg_31 = ip+8x4 = x0 | vreg_17call 0x2c04vreg_16 = sp + 0x1c0vreg_17 = sp + 0x1c8vreg_18 = sp + 0x1d0vreg_19 = sp + 0x1d8vreg_20 = sp + 0x1e0vreg_21 = sp + 0x1e8vreg_22 = sp + 0x1f0vreg_23 = sp + 0x1f8vreg_30 = sp + 0x200vreg_31 = sp + 0x208sp = sp + 0x210 |
对调用外部函数的分析
1 2 3 4 5 6 7 8 9 10 11 | 246 @ 00003c18 x0 = [x4].q // 跳转的 地址 247 @ 00003c1c x1 = [x5].q // 跳转的参数指针 248 @ 00003c20 x24 = x4 249 @ 00003c24 x22 = x5 250 @ 00003c28 call(x20) // call_trampline 0x2c0ccall_register_trampoline_2c0c: 0 @ 00002c0c x2 = x0 1 @ 00002c10 x0 = x1? 2 @ 00002c14 jump(x2) |
0x2c0c 是跳板函数,x4 存储的是要调用的函数的地址, x5 存储的是被调用函数的参数指针,每次调用外部函数的时候都会给 x4,x5赋值,在binja中把llil整个函数复制下来,搜索当前函数可以得到x4,x5在哪使用的
1 2 | 00002c50 void* x4 = x19 - 0x11000002c54 void* x5 = x19 - 0x108 |
所以只需要给 x19 - 0x110 和 x19 - 0x108 赋值,就可以控制好调用外部函数了,通过阅读 金罡 大佬的文章获得了 vm解释器的内存布局

可以看到 x4 就是 vreg4,x5就是 vreg5,所以给 vreg4 vreg5 设置值就可以完成跳转
这里拿出我还原的代码,第一次调用外部函数开始解释
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 | sp = sp + -0x210sp + 0x208 = vreg_31sp + 0x200 = vreg_30sp + 0x1f8 = vreg_23sp + 0x1f0 = vreg_22sp + 0x1e8 = vreg_21sp + 0x1e0 = vreg_20sp + 0x1d8 = vreg_19sp + 0x1d0 = vreg_18sp + 0x1c8 = vreg_17sp + 0x1c0 = vreg_16vreg_16 = x0 | vreg_7vreg_11 = x4 + 0x10vreg_12 = x4 + 0x0vreg_2 = vreg_6 + 0x0x3 = vreg_6 + 0x8vreg_5 = vreg_6 + 0x10vreg_7 = vreg_6 + 0x18vreg_8 = vreg_6 + 0x20vreg_9 = vreg_6 + 0x28vreg_18 = x4 + 0x18vreg_10 = vreg_6 + 0x30vreg_6 = vreg_6 + 0x38vreg_19 = x4 + 0x8x1 = vreg_19 + 0x76sp + 0x88 = x1x4 = vreg_18 + 0x0branch0(先不处理)未知x1 = 0xffffffffff220000x1 = x1 | (0x5870 & 0xffff)x4 = x1 + vreg_6sp + 0x40 = x4x4 = x1 + vreg_10sp + 0x38 = x4x4 = x1 + vreg_9sp + 0x30 = x4x4 = x1 + vreg_8sp + 0x20 = x4x4 = x1 + vreg_7sp + 0x18 = x4x4 = x1 + vreg_5sp + 0x0 = x4vreg_23 = x1 + x3vreg_30 = x1 + vreg_2vreg_20 = x0 + 0x20sp + 0x1b0 = vreg_20vreg_5 = sp + 0x1b0x4 = x0 | vreg_30vreg_25 = x0 | vreg_16sp + 0x28 = vreg_11vreg_31 = ip+8sp + 0x8 = vreg_12call 0x2b40 |
tips: 部分虚拟寄存器的做了改变(让分析起来更加方便), x4 就是 vreg4, x5就是 vreg5, sp就是 vreg29,上面伪代码中的 branch 是产生分支的位置,因为我没得打算完整还原所有分支,所以忽略了,未知的位置一般都是设置标志位,也可以忽略
调用的 0x2b40 是被前面的 vm代码计算出来的,我用angr符号执行的时候直接得到了地址,然后记录了下来,先来验证下我符号执行的得到的 伪代码的正确性
x4就是存储的跳转的地址,拿到上面给x4设置值的部分进行分析
1 2 3 4 5 | x1 = 0xffffffffff220000x1 = x1 | (0x5870 & 0xffff)vreg_2 = vreg_6 + 0x0vreg_30 = x1 + vreg_2x4 = x0 | vreg_30 |
总的来说,x4是 x0 | vreg_30
x0 = 0, vreg_30 = x1 + vreg_2
vreg_6 根据上面内存布局知道 是 ext_func_list_21d10,这里面全部都是被加密的外部函数地址
1 2 3 4 5 6 7 8 9 10 11 12 13 | 00021d10 void* vm2_external_func_list_21d10 = 0xddd2d000021d18 void* data_21d18 = 0xddd2ec00021d20 void* data_21d20 = 0xddd32400021d28 void* data_21d28 = 0xddd33800021d30 void* data_21d30 = 0xddd34800021d38 void* data_21d38 = 0xddd35800021d40 void* data_21d40 = 0xddd37800021d48 void* data_21d48 = 0xddd39400021d50 void* data_21d50 = 0xddec5400021d58 void* data_21d58 = 0xddec7000021d60 void* data_21d60 = 0xddec8000021d68 void* data_21d68 = 0xddec9000021d70 void* data_21d70 = 0xdded44 |
vreg_6 + 0x0 = 0xddd2d0
x1是个常数 0xffffffffff225870
1 2 3 | >>> hex(0xffffffffff225870 + 0xddd2d0)'0x10000000000002b40'>>> |
vreg 最大是 8字节 ,所以溢出的不管, 这个值就是 2b40,跟 angr 符号执行得到的结果是对上的
继续分析参数
1 2 3 | vreg_20 = x0 + 0x20sp + 0x1b0 = vreg_20vreg_5 = sp + 0x1b0 |
vreg_5 最终就等于 0x20
这里看一下 金罡 大佬 还原的汇编
1 | 000000D8 8320FACB 11(0x0B) 43(0x2B) BR x25 ;LR=lr,------> pRandNumber=vm2_malloc(0x20) |
汇编中显示的就是 x20,所以基本确定我还原的是没问题的
再来看一下 0x2bd0 对应的 汇编
1 2 3 4 5 6 7 8 9 10 11 | 0 @ 00002b40 sp = sp - 0x10 1 @ 00002b40 [sp {__saved_x19}].q = x19 2 @ 00002b40 [sp + 8 {__saved_x30}].q = x30 3 @ 00002b44 x19 = x0 4 @ 00002b48 x0 = [x19].q 5 @ 00002b4c call(malloc) 6 @ 00002b50 [x19 + 8].q = x0 7 @ 00002b54 x19 = [sp {__saved_x19}].q 8 @ 00002b54 x30 = [sp + 8 {__saved_x30}].q 9 @ 00002b54 sp = sp + 0x1010 @ 00002b58 <return> jump(x30) |
可以看到函数的返回值是放在了 [x19 + 8].q 的,x19就是上面 的x5
再稍微多拿一点 还原的伪代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | vreg_20 = x0 + 0x20sp + 0x1b0 = vreg_20vreg_5 = sp + 0x1b0x4 = x0 | vreg_30vreg_25 = x0 | vreg_16sp + 0x28 = vreg_11vreg_31 = ip+8sp + 0x8 = vreg_12call 0x2b40sp + 0x1a8 = vreg_20vreg_21 = sp + 0x1b8sp + 0x1a0 = vreg_21vreg_5 = sp + 0x1a0vreg_25 = x0 | vreg_16vreg_31 = ip+8x4 = x0 | vreg_23call 0x2b5c |
调用 0x2b5c 准备的参数 x5 是 sp + 0x1a0 = vreg_21 = sp + 0x1b8
调用 0x2b40 准备的参数 x5 是 sp + 0x1b0
0x1b8 = 0x1b0 + 8
所以这里可以看到 调用 0x2b40的参数是 0x2b5c的返回值,逻辑上基本与金罡大佬文章中还原的代码一致(下面的就是,randNumberSize我没写出来,但读者可以试着分析下我还原的伪代码,不难得出也是 0x20)
1 2 3 4 5 6 7 8 9 10 11 12 | 000000CC 03C021CB 11(0x0B) 07(0x07) ORR x4, x0, x30 ;准备跳板目标: base + 0x2B40 = vm2_malloc000000D0 8200C1CB 11(0x0B) 07(0x07) ORR x25, x0, x16 ;跳板函数:pCallRegisterTrampolineFunction000000D4 03AB0A17 23(0x17) -------- STR x11, [sp, #0x28] ;pDstBuffer000000DC 03AC0217 23(0x17) -------- STR x12, [sp, #0x8] ;>> pScrBuffer000000D8 8320FACB 11(0x0B) 43(0x2B) BR x25 ;LR=lr,------> pRandNumber=vm2_malloc(0x20)000000E0 1BB40A17 23(0x17) -------- STR x20, [sp, #0x1a8] ;arg2: randNumberSize=0x20000000E4 1BB50E28 40(0x28) -------- LDR x21, [sp, #0x1b8] ;取出返回值 pRandNumber000000E8 1BB50817 23(0x17) -------- STR x21, [sp, #0x1a0] ;arg1: pRandNumber000000EC 1BA50815 21(0x15) -------- ADD x5, sp, #0x1a0000000F0 8200C1CB 11(0x0B) 07(0x07) ORR x25, x0, x16000000F8 02E021CB 11(0x0B) 07(0x07) ORR x4, x0, x23 ;准备跳板目标000000F4 8320FACB 11(0x0B) 43(0x2B) BR x25 ;LR=lr, ------> generate_rand(pRandNumber,randNumberSize),生成指定len长度的随机数 |
更多的对我还原 伪代码的验证就不再进行, 借助我还原的伪代码,去分析 原始opcode的执行过程到底做了什么事,是可以很容易分析出来的
开源地址
https://github.com/zhuzhu-Top/de_vm
遇到的问题
本来这部分是准备变写代码的时候边记录的,但是问题太多了,后面就没记录了
- 分发器开始执行当前opcode,但是 没有进入 ip++
按照我的理解,所有 opcode 都要从分发器分发,直到 ip++ 位置的
实际的时候发现,确实 分发器的位置,捕获到的所有 opcode 都是对的,
但是部分 opcode 没有在 ip++ 位置出现

解决:
忘记在 extra_stop_points 里面加 ip++位置的偏移,以为会停在那,实际没有,所以 以后想在哪
里做处理,都需要添加上去
,学习了。