关于 VMProtect,从其诞生到现在已经十几年。无数人投入精力进行研究,虚拟机基本结构已经基本明确了。
相关资料可以参考:
[专题][Fight Against Big Four]汇集所有能帮助你对抗强壳的知识(VMP、SE、THEMIDA、Enigma)
顺便推一下我整理的一份虚拟化保护相关资料的列表,放在了Github上,https://github.com/lmy375/awesome-vmp 。
对于 VMProtect 和 Themida 的虚拟机结构,许多文章已经说的得很清楚。然而却少有文章具体的分析方法。
([翻译]手把手静态分析FinSpy 系列文章有细致介绍作者分析FinSpy VM 的分析过程与思考过程,是非常值得参考的。)
假如我们面对野生样本中的未知虚拟机,该如何入手,一步一步弄清虚拟机结构,提取字节码,进行代码还原?
本系列会分析多个不同类型的虚拟机样本(VMProtet, Code Virtualizer 甚至更多有趣的 VM 保护样本),向大家展示我自己针对虚拟机保护代码的分析方法。
本文是系列的第1篇,内容上没有什么很新的东西,主要是展示一下完整的分析过程。
本文中通过 Trace 提取虚拟指令的部分我个人觉得还算有趣,对虚拟机已经有了解的读者可以跳过其他废话直接看那一部分。
对于大多数虚拟机来说,其结构是相似的。
-
VM_DATA 是虚拟机字节码,是虚拟机要解释执行的指令。
-
VM_EIP,也可以叫 VPC 或者 vEIP ,比如 VMProtect 中的 ESI 寄存器。一般是指向 VM_DATA 中的某个地址,虚拟机每次从这里取出指令,并执行。
-
VM_CONTEXT 虚拟机上下文,实际就是虚拟机寄存器数组。比如 VMProtect 中的 EDI 寄存器的地址,就是虚拟机寄存器数组的起始地址。
-
VM_STACK 虚拟栈,栈式虚拟机实现起来方便,膨胀倍数高,是虚拟机保护的首选。虚拟栈就是临时进行数据交换。VMProtect 的 EBP 寄存器就是虚拟栈的栈顶指针。
一般虚拟机保护代码的执行过程是这样的:
- 初始化虚拟机。保存物理寄存器到虚拟机CONTEXT中。
- 从 VM_EIP 处取出指针,根据指令的 Opcode 跳转到 Handler 代码中
- 执行 Handler,每条 Handler 完成不一样的功能,代表不同的指令操作。 一般来说是操作虚拟寄存器或虚拟栈,进行一些算术运算等。
- 执行完 Handler 后,VM_EIP 会向后移动,指向下一条指令。回到第2步,解释执行下一条指令。
- 执行完全部字节码后,虚拟机退出,将虚拟寄存器的值还原到物理寄存器中。
所以在分析虚拟机保护的过程,把握如下几个关键要素:
- 关键数据结构的位置:虚拟栈在哪?虚拟寄存器在哪儿?
- 解释循环位置:VM_EIP 在哪儿?如何取指令?如何跳向 Handler?
- Handler 分析:有多少条 Handler?每条 Handler 都要完成什么样的工作?
下面实例分析一个虚拟机保护的样本,展示一下分析思路。
为什么选这么古老的版本,而且还是 Demo 版。因为这个版本虚拟机的主体代码没有混淆,保留了完整且清晰的虚拟机结构,适合入门分析。
样本是对如下代码进行 VM 得到的。
1 2 3 4 5 6 7 | sub_401000 proc near
mov eax, dword_403000
add eax, 12345678h
sub eax, 12345678h
mov dword_403000, eax
retn
sub_401000 endp
|
经验丰富的话应该对 VMProtect 虚拟机结构已经比较熟悉,这里并不会介绍新的东西,只是展示一下思路,给新人一点参考。
IDA 打开加保护后的文件,定位到0x401000位置,这是我们进行保护的代码位置。经过虚拟机保护,原本的代码已经不在了。
新的代码如下:
1 2 | .text: 00401000 push offset byte_404781
.text: 00401005 call sub_40472C
|
CALL的目的地址已经不在.text节
中,在新加的.vmp0
中。也就是虚拟机新加入的代码了。
具体代码如下:
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 | .vmp0: 0040472C push esi ; 依次保存寄存器
.vmp0: 0040472D push edi
.vmp0: 0040472E push esp
.vmp0: 0040472F push ebx
.vmp0: 00404730 push eax
.vmp0: 00404731 push edx
.vmp0: 00404732 push ebp
.vmp0: 00404733 pushf
.vmp0: 00404734 push ecx
.vmp0: 00404735 push ds:dword_404649 ; 重定位偏移
.vmp0: 0040473B push 0
.vmp0: 00404740 mov esi, [esp + 30h ] ; 前面 push 的 offset byte_404781
.vmp0: 00404744 mov ebp, esp
.vmp0: 00404746 sub esp, 0C0h ; 分配一下栈空间
.vmp0: 0040474C mov edi, esp ; 栈顶位置赋值给 edi
.vmp0: 0040474E
.vmp0: 0040474E loc_40474E:
.vmp0: 0040474E add esi, [ebp + 0 ] ; 重定位
.vmp0: 00404751
.vmp0: 00404751 loc_404751:
.vmp0: 00404751
.vmp0: 00404751 mov al, [esi] ; 从 esi 取出 1 字节
.vmp0: 00404753 movzx eax, al
.vmp0: 00404756 inc esi ; esi 字节
.vmp0: 00404757 jmp ds:off_40409C[eax * 4 ] ; 根据 esi 取出字节进行跳转
|
这段代码首先保存当前寄存器的值。(这与前面介绍了虚拟机初始化的过程是一致的) 然后进行分配栈空间,初始化 esi, edi, ebp 寄存器。这三个寄存器都是作什么的?
mov al, [esi]
从 esi 地址取出 1 字节,并根据这一字节进行跳转jmp ds:off_40409C[eax*4]
。这一过程很像前面介绍的虚拟机执行过程的第2步。
继续分析验证推断, esi 的值实际来自前面的 mov esi, [esp+30h]
,进一步追溯实际来自push offset byte_404781
。IDA 查看一下 byte_404781
位置:
1 2 3 4 5 6 7 | .vmp0: 00404781 byte_404781 db 0Eh , 0E8h , 81h
.vmp0: 00404784 dd 5C765DA9h , 3E0A1A1Eh , 3A36262Eh , 602122Ah , 3000E822h
.vmp0: 00404784 dd 16790040h , 34567869h , 1E7A1412h , 5678E836h , 97341234h
.vmp0: 00404784 dd 5C066B4Dh , 1121973Eh , 4C3C0622h , 0EB161121h , 1E11F7EAh
.vmp0: 00404784 dd 326B2020h , 78081577h , 36325C32h , 30006904h , 0ED0040h
.vmp0: 00404784 dd 4382010h , 8342C24h , 0E1001Ch , 8 dup( 0 )
.vmp0: 00404800 dd 200h dup(?)
|
初始有值的数据,且位于 .vmp0
节内。这很有可能就是 VM_DATA
也就是虚拟机字节码的内容(实际也确实是这样的)。那么 esi 寄存器的作用也就明确了,就是VM_EIP
。
edi 和 ebp 是指向栈上的内存,具体是什么还不明确,继续分析。
前面那个jmp ds:off_40409C[eax*4]
跳转是个很典型的switch结构,IDA可以查看CFG图如下:

图中蓝色线条最为密集的部分就是 0x0404751 的代码,这部分代码前面已经分析过,是 esi 取字节并跳转。
可以看到跳转的目标很多,共有41个跳转目标。这时我们有充分的理由认为这个41个跳转目标就是 Handler 代码。
接下来是比较枯燥的过程,要逐条分析每个 Handler。
因为要考虑地址寄存器宽度1字节、2字节、4字节,所以41条Handler中有许多指令功能是一致的,只是数据宽度不同。取几条比较典型的指令说明:
-
立即数压栈
1 2 3 4 5 6 7 | .vmp0: 0040462B vPushImm4:
.vmp0: 0040462B
.vmp0: 0040462B mov eax, [esi]
.vmp0: 0040462D sub ebp, 4
.vmp0: 00404630 lea esi, [esi + 4 ]
.vmp0: 00404633 mov [ebp + 0 ], eax
.vmp0: 00404636 jmp loc_40400F
|
这条指令从 esi 地址取出 4 字节,然后 ebp - 4 后写入 [ebp] 内存处。 esi 指向的地方是 VM_DATA,因此取出的部分是指令中的固定数,即虚拟指令中的立即数。 ebp - 4 后再赋值的操作很像栈操作,先抬高栈顶,再写值。通过分析其他指令可以发现许多加减 ebp 然后读写值的情况。那么可以认定 ebp 就是虚拟栈栈顶指针。
-
寄存器压栈
1 2 3 4 5 6 7 | .vmp0: 004045AF vPushReg4:
.vmp0: 004045AF
.vmp0: 004045AF and al, 3Ch
.vmp0: 004045B2 mov edx, [edi + eax]
.vmp0: 004045B5 sub ebp, 4
.vmp0: 004045B8 mov [ebp + 0 ], edx
.vmp0: 004045BB jmp loc_40400F
|
这条指令取 al 的后几位,作为 edi 寄存器的偏移,取出值后压入栈顶。al 是之前从 esi 地址中取出的值,也是指令的一部分。由该值作索引,从 edi 寻址取值。可以猜测 edi 就是 VM_CONTEXT。这里是将虚拟寄存器中值压入虚拟栈中。
-
计算
1 2 3 4 5 6 7 | .vmp0: 00404000 vAdd4:
.vmp0: 00404000
.vmp0: 00404000 mov eax, [ebp + 0 ]
.vmp0: 00404003 add [ebp + 4 ], eax
.vmp0: 00404006 pushf
.vmp0: 00404007 pop dword ptr [ebp + 0 ]
.vmp0: 0040400A jmp loc_404751
|
这是一条比较明显的计算指令(加法指令)。
ebp + 0 是栈顶, ebp + 4 是次栈顶。 二者相加,保存在 [ebp + 4]。 eflag 值保存在 [ebp + 0]。
即先从栈中弹出两个数,相加后将结果压入栈中,再将eflag值压入栈中。
-
其他
我们已经确定了:
- esi 取出的值来自指令,因此是立即数或者寄存器下标
- edi 指向VM_CONTEXT,读写就是操作虚拟寄存器
- ebp 指向虚拟栈顶,读写就是压栈、弹栈
根据这几点,很容易就可以将所有 Handler 的作用分析清楚。
分析完所有 Handler 之后,就可以提取分析字节码了。
[招生]科锐逆向工程师培训(2025年3月11日实地,远程教学同时开班, 第52期)!
最后于 2018-4-9 18:12
被穆恩编辑
,原因: