1 引言
几年来,逆向一直停留在CTF坐牢水平,脱壳也就会用工具、最多说说原理。所以决定开始“深入”了解下脱VMP壳。当然,毕竟要循序渐进,我们还是从最简单的开始。这里我们选择了VMP1.1版本,自己编写简单的程序并用其加壳,将原程序与加壳后程序进行对比分析。
2 环境
- 宿主机:Windows 11 x64
- VMWare 17
- Visual Studio 2022
- IDA Pro
- VMP 1.1
- 虚拟机:
3 分析
3.1 示例程序(example1)
1 |
|
3.2 虚拟机保护方法分析
在使用VMP进行加壳的时候可以知道,VMP是对指定的函数进行保护。我们可以先通过IDA查看原程序中foo函数的地址,并且要保证该函数的指令要大于等于5个字节。在这里我们将经过VMP保护的foo函数称为foo_vmp。
foo_vmp函数包含一个jmp指令:
1 | .text:004B3F20 ; int __cdecl foo() |
其中的sub_61C13B位于段.vmp0内,也就是说VMP通过修改原函数的指令,让其进入vmp段内执行来实现接管和保护原函数。
sub_61C13B函数实际执行虚拟机化后的foo函数的指令,其中push offset dword_61C0B4
将foo_vmp的虚拟机指令起始地址(VmpCode)压入栈中,VmpCode也位于vmp段中,是一个byte数组。
pushf、pusha两个指令将当前的标志寄存器和通用寄存器内容压入栈中,push 0
将一个0压入栈,并且它(VmpPC)也用于后续的虚拟机指令寻址。
下面是执行到0061B88D
处(ESP=0019FDE0
),栈中可能的数据:
1 | Stack[00001CEC]:0019FDE0 dd 0 ; VmpPC |
函数foo_vmp逻辑如下:
1 | .vmp0:0061C127 sub_61C127 proc far ; CODE XREF: foo(void)↑j |
在0061B8E2
处的jmp ds:jpt_61B8E2[eax*4]
用到一个跳表(jump table),EAX寄存器的值是其索引。EAX的值来源于AL寄存器与BL寄存器进行的一系列计算,而AL寄存器的值是通过访问VmpCode + VmpPC
地址的字节得到的。
跳表存储的内容如下:
1 | .vmp0:0061BBEB jpt_61B8E2 dd offset loc_61B769, offset loc_61B5A4, offset loc_61BAA0 |
3.3 虚拟机指令分析
在最开始,foo_vmp将ESP指向的栈中的数据复制到EDI寄存器指向vmp段中一个固定区域,包括VmpPC、pushf和pusha压入栈中的数据。由于每次只能复制1个DWORD,所以(1)(2)对应的代码会执行多次。
(1)首先将VmpPC复制到地址edi+eax*4
处:
1 | .vmp0:0061B77D lodsb ; jumptable 0061B8E2 case 213 |
存放位置如下:
1 | .vmp0:0061742C dd 0 ; VmpPC |
(2)随后将pusha指令压入栈中的数据复制到地址edi+eax*4
处:
1 | .vmp0:0061B77D loc_61B77D: ; CODE XREF: sub_61C127-845↓j |
存放位置如下:
1 | .vmp0:0061742C dd 0 ; VmpPC |
(3)将VmpCode地址pop到EAX寄存器:
1 | .vmp0:0061B593 loc_61B593: ; CODE XREF: sub_61C127-845↓j |
栈内容如下:
1 | Stack[00001CEC]:0019FE08 dd offset dword_61C0B4 ; &VmpCode |
EAX寄存器值为:0x0061C0B4
。
(4)压栈(DWORD)操作:
1 | .vmp0:0061B73D loc_61B73D: ; CODE XREF: sub_61C127-845↓j |
lodsd指令取出一个DWORD作为操作数,此处为0A110DF68
。经计算后,EAX值为0xE01EC2A2
,EBX值为0xE080835C
。并将EAX值压入栈中。
栈数据如下:
1 | 0019FE08 E01EC2A2 ; <==== ESP |
然后,foo_vmp会再执行一次(4)的压栈操作,这次压入了1FE13D5E
。
(5)压栈(WORD)操作:
1 | .vmp0:0061BA83 lodsb ; jumptable 0061B8E2 case 24 |
最开始的lodsb指令与lodsd类似,只不过是取出一个BYTE。最后,指令push ax
压入了0x02
。
然后,foo_vmp压入了一个DWORD值为186E6D
。
(6)左移操作:
1 | .vmp0:0061C055 loc_61C055: ; CODE XREF: sub_61C127-845↑j |
从栈中弹出1个DWORD到EDX,弹出1个WORD到CX。然后进行左移(shl)操作,即EDX << CX
,然后将结果压入栈中。
此处压入的值为61B9B4
,即0x186E6D << 2
的结果。
另外提及一下,后面还会有一种将标志寄存器也压入栈中的左移操作。如下所示:
1 | .vmp0:0061BB99 loc_61BB99: ; CODE XREF: sub_61C127-845↑j |
(7)EDI寻址压栈操作
1 | .vmp0:0061B829 loc_61B829: ; CODE XREF: sub_61C127-845↓j |
取出大小为1BYTE的操作数,经过计算后EAX值为0x0000000B
,利用该值与EDI寄存器进行寻址,这里edi+eax*4
是0,也就是之前push 0
指令经过压栈然后复制到EDI所指区域产生的值。
(8)加法操作
1 | .vmp0:0061B550 loc_61B550: ; CODE XREF: sub_61C127-845↓j |
首先从栈上弹出1个DWORD到ECX,然后将ECX加到栈顶所指的[ESP]中。
(9)寻址压栈操作
1 | .vmp0:0061B719 loc_61B719: ; CODE XREF: sub_61C127-845↓j |
从栈上弹出1个DWORD到EDX,将EDX的值作为内存地址进行寻址,并将访存结果压入栈中。
这里访存结果是E99C6650
,是某些指令机器码的一部分。
(10)压入栈顶指针操作:
1 | .vmp0:0061BA03 loc_61BA03: ; CODE XREF: sub_61C127-845↑j |
(11)栈顶访存操作
1 | .vmp0:0061BB61 loc_61BB61: ; CODE XREF: sub_61C127-845↑j |
将栈顶DWORD弹出到EDX寄存器,再将[EDX]
访存结果压入栈顶。
我们现在还是初步分析VMP,所以接下来我们略过重复的虚拟机指令。
(12)返回(ret)操作
1 | .vmp0:0061BAF9 loc_61BAF9: ; CODE XREF: .vmp0:0061B8E2↑j |
将栈顶弹出到EAX作为函数的返回值,用popa、popf指令还原寄存器。
4 总结
程序经过VMP保护后,函数的逻辑使用虚拟机指令重写。人工翻译虚拟机指令的工作量太大,所以下一步需要编写工具来实现自动翻译虚拟机指令,供我们阅读。