序言
(本贴也发布于看雪:https://bbs.pediy.com/thread-225262.htm)
关于 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 都要完成什么样的工作?
实例分析 VMProtect 1.81 Demo
下面实例分析一个虚拟机保护的样本,展示一下分析思路。
为什么选这么古老的版本,而且还是 Demo 版。因为这个版本虚拟机的主体代码没有混淆,保留了完整且清晰的虚拟机结构,适合入门分析。
样本是对如下代码进行 VM 得到的。
sub_401000 proc near
mov eax, dword_403000
add eax, 12345678h
sub eax, 12345678h
mov dword_403000, eax
retn
sub_401000 endp
经验丰富的话应该对 VMProtect 虚拟机结构已经比较熟悉,这里并不会介绍新的东西,只是展示一下思路,给新人一点参考。
1. 初步结构
IDA 打开加保护后的文件,定位到0x401000位置,这是我们进行保护的代码位置。经过虚拟机保护,原本的代码已经不在了。
新的代码如下:
.text:00401000 push offset byte_404781
.text:00401005 call sub_40472C
CALL的目的地址已经不在.text节
中,在新加的.vmp0
中。也就是虚拟机新加入的代码了。
具体代码如下:
.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
位置:
.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 代码。
2. Handler 分析
接下来是比较枯燥的过程,要逐条分析每个 Handler。
因为要考虑地址寄存器宽度1字节、2字节、4字节,所以41条Handler中有许多指令功能是一致的,只是数据宽度不同。取几条比较典型的指令说明:
- 立即数压栈
.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 就是虚拟栈栈顶指针。
- 寄存器压栈
.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。这里是将虚拟寄存器中值压入虚拟栈中。
- 计算
.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 的作用分析清楚。
3. 还原字节码
分析完所有 Handler 之后,就可以提取分析字节码了。
方法1:IDA 静态解析 VM_DATA
IDA 查看 VM_DATA 的内容如下:
00404781 0E E8 81 A9 5D 76 5C 1E 1A 0A 3E 2E 26 36 3A 2A ....]v\...>.&6:*
00404791 12 02 06 22 E8 00 30 40 00 79 16 69 78 56 34 12 ..."..0@.y.ixV4.
004047A1 14 7A 1E 36 E8 78 56 34 12 34 97 4D 6B 06 5C 3E .z.6.xV4.4.Mk.\>
004047B1 97 21 11 22 06 3C 4C 21 11 16 EB EA F7 11 1E 20 .!.".<L!.......
004047C1 20 6B 32 77 15 08 78 32 5C 32 36 04 69 00 30 40 k2w..x2\26.i.0@
004047D1 00 ED 00 10 20 38 04 24 2C 34 08 1C 00 E1 00 00 .... 8.$,4......
004047E1 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
跳转指令jmp ds:off_40409C[eax*4]
,跳转表就在0x0040409C处。如下:
.vmp0:0040409C handlers dd offset vPushReg4
.vmp0:004040A0 dd offset vShl2
.vmp0:004040A4 dd offset vPopReg4
.vmp0:004040A8 dd offset vPushImm2
.vmp0:004040AC dd offset vPushReg4
.vmp0:004040B0 dd offset vNor2
.vmp0:004040B4 dd offset vPopReg4
.vmp0:004040B8 dd offset vShl1
.vmp0:004040BC dd offset vPushReg4
.vmp0:004040C0 dd offset vWriteMemDs4
.vmp0:004040C4 dd offset vPopReg4
.vmp0:004040C8 dd offset vShr2
.vmp0:004040CC dd offset vPushReg4
.vmp0:004040D0 dd offset vNor2
.vmp0:004040D4 dd offset vPopReg4 <---
.vmp0:004040D8 dd offset vPopEBP
.vmp0:004040DC dd offset vPushReg4
....
VM_DATA 第1个字节是 0E。跳转的位置是 0x40409C + 0x0E * 4 = 0x4040d4
即上面的 vPopReg4 指令。Handler 代码如下:
.vmp0:00404058 and al, 3Ch
.vmp0:0040405B mov edx, [ebp+0] ; 取栈顶值
.vmp0:0040405E add ebp, 4 ; 弹栈
.vmp0:00404061 mov [edi+eax], edx ; 值写入VM_CONTEXT[al]
.vmp0:00404064 jmp loc_404751
0x0E & 0x3C = 0x0C
。 4 字节一个寄存器,0x0C/4 = 3
。因此这里是将栈中的值写入第3个寄存器(记为R3)。
整条指令就可以记为 vPushReg4 R3
。
VM_DATA 下一个字节是 E8 ,跳转的目标在
.vmp0:0040443C dd offset vPushImm4
Handler 代码如下:
.vmp0:0040462B vPushImm4:
.vmp0:0040462B
.vmp0:0040462B mov eax, [esi] ; 取4字节立即数
.vmp0:0040462D sub ebp, 4 ; 栈分析4字节空间
.vmp0:00404630 lea esi, [esi+4] ; esi = esi + 4
.vmp0:00404633 mov [ebp+0], eax ; 压入栈顶
.vmp0:00404636 jmp loc_40400F
取 4 字节 81 A9 5D 76
, 对应数字 0x765da981,这条指令就可以记作 vPushImm4 0x765da981
依次类推,通过编写IDAPython脚本,可以自动的解析 VM_DATA。最终还原出所有虚拟指令。
方法2: OD Trace 分析
静态分析确实可以还原出虚拟指令,但是动态运行可以更快更容易的得到结果。
我们已经分析出了每个 Handler 的位置。那么我们只需要确定每个 Handler 调用的序列,就可以还原出字节码。
这里我们使用 Ollydbg 2.0 的 Trace 功能(不使用 OD 1 的原因是 2 的 Trace 可以记录内存引用,功能更为强大)。
- OD2 载入样本
- 切换 E (Executable modules) 窗口,右键主模块,选择
Limit run trace protocol to selected module
。表示 Trace 时记录该模块的所有地址,其他模块(如系统模块)不进行记录。
- Options -> Run Trace -> 勾选 Remember memory ,记录内存
- Trace -> Trace into 或 Ctrl + F11 开始 Trace
- View -> Run trace 打开 Trace 窗口,右键 Log to file (勾选Add available contents 和 Separate columns with tabs)保存文件。
得到的 Trace 类似如下内容:
main 00401000 push 00404781 [0019FF7C]=00000000 EAX=099B4127, ECX=00401015, EDX=00401015, EBX=00344000, ESP=0019FF80, EBP=0019FF94, ESI=00401015, EDI=00401015
main 00401005 call 0040472C EAX=099B4127, ECX=00401015, EDX=00401015, EBX=00344000, ESP=0019FF7C, EBP=0019FF94, ESI=00401015, EDI=00401015
main 0040472C push esi [0019FF74]=00000000 EAX=099B4127, ECX=00401015, EDX=00401015, EBX=00344000, ESP=0019FF78, EBP=0019FF94, ESI=00401015, EDI=00401015
main 0040472D push edi [0019FF70]=00000000 EAX=099B4127, ECX=00401015, EDX=00401015, EBX=00344000, ESP=0019FF74, EBP=0019FF94, ESI=00401015, EDI=00401015
main 0040472E push esp [0019FF6C]=00000000 EAX=099B4127, ECX=00401015, EDX=00401015, EBX=00344000, ESP=0019FF70, EBP=0019FF94, ESI=00401015, EDI=00401015
main 0040472F push ebx [0019FF68]=00000000 EAX=099B4127, ECX=00401015, EDX=00401015, EBX=00344000, ESP=0019FF6C, EBP=0019FF94, ESI=00401015, EDI=00401015
main 00404730 push eax [0019FF64]=00000000 EAX=099B4127, ECX=00401015, EDX=00401015, EBX=00344000, ESP=0019FF68, EBP=0019FF94, ESI=00401015, EDI=00401015
main 00404731 push edx [0019FF60]=00000000 EAX=099B4127, ECX=00401015, EDX=00401015, EBX=00344000, ESP=0019FF64, EBP=0019FF94, ESI=00401015, EDI=00401015
main 00404732 push ebp [0019FF5C]=00000000 EAX=099B4127, ECX=00401015, EDX=00401015, EBX=00344000, ESP=0019FF60, EBP=0019FF94, ESI=00401015, EDI=00401015
main 00404733 pushfd [0019FF58]=00000000 EAX=099B4127, ECX=00401015, EDX=00401015, EBX=00344000, ESP=0019FF5C, EBP=0019FF94, ESI=00401015, EDI=00401015
main 00404734 push ecx [0019FF54]=00000000 EAX=099B4127, ECX=00401015, EDX=00401015, EBX=00344000, ESP=0019FF58, EBP=0019FF94, ESI=00401015, EDI=00401015
main 00404735 push dword ptr ds:[404649] [00404649]=00000000 EAX=099B4127, ECX=00401015, EDX=00401015, EBX=00344000, ESP=0019FF54, EBP=0019FF94, ESI=00401015, EDI=00401015
main 0040473B push 0 [0019FF4C]=00000000 EAX=099B4127, ECX=00401015, EDX=00401015, EBX=00344000, ESP=0019FF50, EBP=0019FF94, ESI=00401015, EDI=00401015
main 00404740 mov esi, dword ptr ss:[esp+30] [0019FF7C]=00404781 EAX=099B4127, ECX=00401015, EDX=00401015, EBX=00344000, ESP=0019FF4C, EBP=0019FF94, ESI=00401015, EDI=00401015
main 00404744 mov ebp, esp EAX=099B4127, ECX=00401015, EDX=00401015, EBX=00344000, ESP=0019FF4C, EBP=0019FF94, ESI=00404781, EDI=00401015
main 00404746 sub esp, 0C0 EAX=099B4127, ECX=00401015, EDX=00401015, EBX=00344000, ESP=0019FF4C, EBP=0019FF4C, ESI=00404781, EDI=00401015
main 0040474C mov edi, esp EAX=099B4127, ECX=00401015, EDX=00401015, EBX=00344000, ESP=0019FE8C, EBP=0019FF4C, ESI=00404781, EDI=00401015
main 0040474E add esi, dword ptr ss:[ebp] [0019FF4C]=00000000 EAX=099B4127, ECX=00401015, EDX=00401015, EBX=00344000, ESP=0019FE8C, EBP=0019FF4C, ESI=00404781, EDI=0019FE8C
main 00404751 mov al, byte ptr ds:[esi] [00404781]=0E EAX=099B4127, ECX=00401015, EDX=00401015, EBX=00344000, ESP=0019FE8C, EBP=0019FF4C, ESI=00404781, EDI=0019FE8C
main 00404753 movzx eax, al EAX=099B410E, ECX=00401015, EDX=00401015, EBX=00344000, ESP=0019FE8C, EBP=0019FF4C, ESI=00404781, EDI=0019FE8C
main 00404756 inc esi EAX=0000000E, ECX=00401015, EDX=00401015, EBX=00344000, ESP=0019FE8C, EBP=0019FF4C, ESI=00404781, EDI=0019FE8C
main 00404757 jmp dword ptr ds:[eax*4+40409C] [004040D4]=00404058 EAX=0000000E, ECX=00401015, EDX=00401015, EBX=00344000, ESP=0019FE8C, EBP=0019FF4C, ESI=00404782, EDI=0019FE8C
...
每行的格式比较统一,因此可以很容易的写脚本进行处理。
根据前面人肉分析的结果,已经确定了每个Handler的地址。
0x0404000: 'vAdd4',
0x0404041: 'vNor2',
0x0404058: 'vPopReg4',
0x0404069: 'vReadMemSs4',
0x0404077: 'vShr4',
0x040408E: 'vRet',
....(等)
综合这些信息,我们逐条检查每一条Trace,如果地址是0x0404000,则说明在执行vAdd4指令,如果地址是0x0404041,则说明在执行vNor2指令,依次类推,写脚本处理,得到伪代码序列,如下:
# python dump_vmp1.81.py
vPopReg4
vPushImm4
vAdd4
vPopReg4
vPopReg4
vPopReg4
vPopReg4
vPopReg4
vPopReg4
vPopReg4
vPopReg4
vPopReg4
vPopReg4
...(等)
不过还有一点不足,比如vPopReg4
指令,这里只知道指令类型,具体的操作数还是不知道的。根据前面的分析我们知道vPopReg4
操作的寄存器是由 al & 0x3c 决定的。因此我们只要再查找下 Trace 中执行到 vPopReg4 时 eax 的值,就可以计算出操作寄存器的下标了。比如第1次调用 vPopReg4
时的 Trace 如下:
.vmp0:00404058 vPopReg4:
main 00404058 and al, 3C EAX=0000000E, ECX=00401015, EDX=00401015, EBX=00344000, ESP=0019FE8C, EBP=0019FF4C, ESI=00404782, EDI=0019FE8C
main 0040405B mov edx, dword ptr ss:[ebp] [0019FF4C]=00000000 EAX=0000000C, ECX=00401015, EDX=00401015, EBX=00344000, ESP=0019FE8C, EBP=0019FF4C, ESI=00404782, EDI=0019FE8C
main 0040405E add ebp, 4 EAX=0000000C, ECX=00401015, EDX=00000000, EBX=00344000, ESP=0019FE8C, EBP=0019FF4C, ESI=00404782, EDI=0019FE8C
main 00404061 mov dword ptr ds:[eax+edi], edx [0019FE98]=00000000 EAX=0000000C, ECX=00401015, EDX=00000000, EBX=00344000, ESP=0019FE8C, EBP=0019FF50, ESI=00404782, EDI=0019FE8C
main 00404064 jmp 00404751 EAX=0000000C, ECX=00401015, EDX=00000000, EBX=00344000, ESP=0019FE8C, EBP=0019FF50, ESI=00404782, EDI=0019FE8C
EAX 的值为 0x1e,那么操作的虚拟寄存器下标就是 (0x0e & 0x3c)/4 = 3,完整的指令应该是vPopReg4 R3
。
再举个vPushImm4
的例子,第1次调用 vPushImm4
的 Trace如下:
main 0040462B mov eax, dword ptr ds:[esi] [00404783]=765DA981 EAX=000000E8, ECX=00401015, EDX=00000000, EBX=00344000, ESP=0019FE8C, EBP=0019FF50, ESI=00404783, EDI=0019FE8C
main 0040462D sub ebp, 4 EAX=765DA981, ECX=00401015, EDX=00000000, EBX=00344000, ESP=0019FE8C, EBP=0019FF50, ESI=00404783, EDI=0019FE8C
main 00404630 lea esi, [esi+4] [00404787]=5C EAX=765DA981, ECX=00401015, EDX=00000000, EBX=00344000, ESP=0019FE8C, EBP=0019FF4C, ESI=00404783, EDI=0019FE8C
main 00404633 mov dword ptr ss:[ebp], eax [0019FF4C]=00000000 EAX=765DA981, ECX=00401015, EDX=00000000, EBX=00344000, ESP=0019FE8C, EBP=0019FF4C, ESI=00404787, EDI=0019FE8C
main 00404636 jmp 0040400F EAX=765DA981, ECX=00401015, EDX=00000000, EBX=00344000, ESP=0019FE8C, EBP=0019FF4C, ESI=00404787, EDI=0019FE8C
调用mov eax, dword ptr ds:[esi]
后,EAX的值在下一条指令中体现,是EAX=765DA981
,那么完整的指令是vPushImm4 765DA981
通过这种方法,可补全指令中的立即数和寄存器下标,可以得取的最终虚拟指令序列如下:
vPopReg4 R3
vPushImm4 0x765da981
vAdd4
vPopReg4 R7
vPopReg4 R6
vPopReg4 R2
vPopReg4 R15
vPopReg4 R11
vPopReg4 R9
vPopReg4 R13
vPopReg4 R14
vPopReg4 R10
vPopReg4 R4
vPopReg4 R0
vPopReg4 R1
vPopReg4 R8
vPushImm4 0x403000
vReadMemDs4
vPopReg4 R5
vPushImm4 0x12345678
vPushReg4 R5
vAdd4
vPopReg4 R7
vPopReg4 R13
vPushImm4 0x12345678
vPushReg4 R13
vPushEBP
vReadMemSs4
vNor4
vPopReg4 R1
vAdd4
vPopReg4 R15
vPushEBP
vReadMemSs4
vNor4
vPopReg4 R8
vPopReg4 R1
vPushReg4 R15
vPushEBP
vReadMemSs4
vNor4
vPopReg4 R5
vPushImmSx2 0xfffff7ea
vNor4
vPopReg4 R7
vPushReg4 R8
vPushReg4 R8
vNor4
vPopReg4 R12
vPushImmSx2 0x815
vNor4
vPopReg4 R12
vAdd4
vPopReg4 R12
vPopReg4 R13
vPushReg4 R1
vPushImm4 0x403000
vWriteMemDs4
vPushReg4 R0
vPushReg4 R4
vPushReg4 R8
vPushReg4 R14
vPushReg4 R1
vPushReg4 R9
vPushReg4 R11
vPushReg4 R13
vPushReg4 R2
vPushReg4 R7
vPushReg4 R0
vRet
结果和 VMP分析插件1.4 得到的结果是一样的。(本文虚拟指令的表示方式也参考了VMP分析插件1.4中表示方法。)
4. 还原x86代码
这部分一直都是十分困难的点。比较典型的方法是通过模板进行匹配收缩,VMP分析插件 1.4 就是这么做的。
不想收集模板,则可以利用编译优化的方法,见我之前写过的[原创]通过编译优化进行VMP代码还原。不过这种方法准确性要比模板匹配差一些。
具体的自动化方法本文不再讨论。因为使用的示例程序比较简单,所以只简单人工还原一下。
注意虚拟机入口处的寄存器入栈顺序:
.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:reloc
.vmp0:0040473B push 0
根据这个还原:
vPopReg4 R3 ; R3 = 0
vPushImm4 0x765da981 ;
vAdd4 ;
vPopReg4 R7 ; R7 = eflag
vPopReg4 R6 ; R6 = 0x765da981
vPopReg4 R2 ; R2 = ECX
vPopReg4 R15 ; R15 = EFLAG
vPopReg4 R11 ; R11 = EBP
vPopReg4 R9 ; R9 = EDX
vPopReg4 R13 ; R13 = EAX
vPopReg4 R14 ; R14 = EBX
vPopReg4 R10 ; R10 = ESP
vPopReg4 R4 ; R4 = EDI
vPopReg4 R0 ; R0 = ESI
vPopReg4 R1 ; push/call 的返回地址 0x40100A
vPopReg4 R8 ; push 的值 即 vm_data 0x404781
// mov eax, dword_403000
vPushImm4 0x403000
vReadMemDs4
vPopReg4 R5 ; R5 = [0x403000]
// add eax, 12345678h
vPushImm4 0x12345678
vPushReg4 R5 ;
vAdd4 ; R5 + 0x12345678
vPopReg4 R7 ; add_flag
vPopReg4 R13 ; R13 = R5 + 0x12345678
// sub eax, 12345678h
// 关键计算公式如下:
// not(a) = nor(a, a)
// and(a, b) = nor(not(a), not(b))
// sub(a, b) = not(not(a) + b) = nor(not(a) + b, not(a) + b) = nor(nor(a, a) + b, nor(a, a) + b)
// 0xfffff7ea = not(0x815)
// not_flag(a) = nor_flag(a, a)
// and_flag(a, b) = nor_flag(a, not(b))
// sub_flag(a, b) = and(FFFFF7EA, not_flag(not(a) + b)) + and(0x815 , add_flag(not(a), b))
vPushImm4 0x12345678 ; 0x12345678 是 b
vPushReg4 R13 ; R13 是 a
vPushEBP
vReadMemSs4 ; 栈顶2个 a
vNor4 ; nor(a, a) = not(a)
vPopReg4 R1 ; R1 = not_flag(a)
vAdd4 ; not(a) + b
vPopReg4 R15 ; R15 = add_flag(not(a) + b)
vPushEBP ;
vReadMemSs4 ; 栈顶2个 not(a) + b
vNor4 ; not(not(a)+b) = sub(a,b)
vPopReg4 R8 ; R8 = not_flag(not(a) + b)
vPopReg4 R1 ; R1 = sub(a,b) = R13 - 0x12345678
vPushReg4 R15 ; R15 = add_flag(not(a) + b)
vPushEBP
vReadMemSs4
vNor4 ; not(R15) = not(add_flag(not(a) + b))
vPopReg4 R5 ; flag - 无用
vPushImmSx2 0xfffff7ea
vNor4 ; nor(0xfffff7ea, R14) = and (0x815, R15) = and(0x815, add_flag(not(a) + b))
vPopReg4 R7 ; R7 = and(0x815, add_flag(not(a) + b)) sub_flag 右半部分
vPushReg4 R8
vPushReg4 R8
vNor4 ; not(R8) = not(not_flag(not(a) + b))
vPopReg4 R12 ; flag - 无用
vPushImmSx2 0x815
vNor4 ; nor(0x815, not(R8)) = and(0xfffff7ea, R8) = and(FFFFF7EA, not_flag(not(a) + b))
vPopReg4 R12 ; flag - 无用
vAdd4 ; add
vPopReg4 R12 ; flag - 无用
vPopReg4 R13 ; R13 = and(FFFFF7EA, not_flag(not(a) + b)) + and(0x815 , add_flag(not(a), b)) = sub_flag(a, b)
// mov dword_403000, eax
vPushReg4 R1
vPushImm4 0x403000 ; [0x403000] = R1
vWriteMemDs4
// ret
vPushReg4 R0 ; ESI
vPushReg4 R4 ; EDI
vPushReg4 R8 ; flag(无用)
vPushReg4 R14 ; EBX
vPushReg4 R1 ; RAX
vPushReg4 R9 ; EDX
vPushReg4 R11 ; EBP
vPushReg4 R13 ; sub_flag
vPushReg4 R2 ; ECX
vPushReg4 R7 ; flag(无用)
vPushReg4 R0 ; ESI(无用)
vRet
刚好可以和 vRet 指令衔接上。
.vmp0:0040408E vRet:
.vmp0:0040408E
.vmp0:0040408E mov esp, ebp
.vmp0:00404090 pop edx ; 被后面的pop覆盖
.vmp0:00404091 pop ebx ; 被后面的pop覆盖
.vmp0:00404092 pop ecx ; R2
.vmp0:00404093 popf ; R13 sub_flag
.vmp0:00404094 pop ebp ; R11
.vmp0:00404095 pop edx ; R9
.vmp0:00404096 pop eax ; R1
.vmp0:00404097 pop ebx ; R14
.vmp0:00404098 pop esi ; 被后面的pop覆盖
.vmp0:00404099 pop edi ; R4
.vmp0:0040409A pop esi ; R0
.vmp0:0040409B retn
小结
本文以一个极弱的虚拟机 VMProtect 1.81 Demo 为例,完整的展示了一个虚拟机保护代码的分析过程。
从初步分析虚拟机结构、到提取Handler,并通过Trace提取虚拟指令序列,最后进行人肉进行代码还原。
作为第1篇,可能有意思的地方并不多。
首先选择的虚拟机版本太弱了,而且是虚拟机内部没有混淆和花指令的。对于正式版 VMProtect 虚拟机的解释执行过程都是添加了冗余指令的,这种情况该如何处理?Code Virtualizer 系列虚拟机内则有代码变形,又如何处理? VMProtect 3.x 已经没有解释循环,使用链式寻址代替,这样有什么方便的提取字节码的方法么?
后面的文章会一点点介绍,希望后面几篇可以让大家有更大的收获。
(文章涉及的样本、IDB文件、Trace文件、部分脚本见附件,密码123456)