VMProtect分析-虚拟化流程
一、没有大炮我们自己造
首先编写一个简单的程序。
1 | int main() |
编译后给程序添加vmp保护壳,这里选择的VMP版本为v3.5.0
。
保护的函数为main函数,编译类型修改为虚拟。
并且关闭一系列保护。
编译完成后,可看到程序体积有一个翻倍增加。
二、只能拉一点点
首先对原程序的汇编进行一个简单查看,用vs在main函数中打下断点运行后,查看反汇编窗口。
1 | 00401040 55 push ebp ;ebp=0x19ff74 |
经过对vmp的分析,可知vmp会将一条代码指令进行拆分后以push和pop命令进行实现,其中push和pop为vmp自实现的函数,这种被vmp自实现用来代替原有指令的函数叫做handler
;解密字节码并跳转到对应的handler,这个过程叫做dispatch
.例如mov ebp,esp的执行在vmp下会被修改为如下代码:
并且vmp会对通用寄存器有自己的解释。
1 | EAX:通用寄存器 |
同时也有定义了自己的寄存器,用来存放一些临时数据。
1 | struct vm_context |
三、时间会告诉你答案
把加了vmp壳的程序拖入OD,能看到属于VMP的入口点。
其中push的内容为被加密了的opcode
,opcode存放着的是所有的handler地址和一些操作寄存器的数据。
1、指令分析
下列所有的代码均为手工取出虚拟化后的代码,并且所有加了v或者v_前缀的内容均值的是vmp自定义的数据。由于vmp在执行函数时会退出虚拟机,函数执行完毕后重新进入虚拟机,因此流程只跟踪到执行printf函数完毕。
VMEntry
1 | 00475751 68 C3365BBC push 0xBC5B36C3 |
VMInit
1 | 00417719 50 push eax ; 保存堆栈 |
vPop r10
1 | 004343B9 8B0E mov ecx,dword ptr ds:[esi] ;读取原始esp |
vPop r12
1 | 0046FF2C 8B0E mov ecx,dword ptr ds:[esi] ;读取原始esp |
vPop r8
1 | 00453841 8B0E mov ecx,dword ptr ds:[esi] ;读取原始esp |
vPop r6
1 | 00430521 8B0E mov ecx,dword ptr ds:[esi] ;读取原始esp |
vPop r3
1 | 0043BD4C 8B0E mov ecx,dword ptr ds:[esi] ;读取原始esp |
vPop r5
1 | 0045E179 8B0E mov ecx,dword ptr ds:[esi] ;读取原始esp |
vPop r9
1 | 0046D3FB 8B0E mov ecx,dword ptr ds:[esi] ;读取原始esp |
vPop r2
1 | 0046C4CB 8B0E mov ecx,dword ptr ds:[esi] ;读取原始esp |
vPop r4
1 | 00474E9C 8B0E mov ecx,dword ptr ds:[esi] ;读取原始esp |
vPop r0
1 | 00476E65 8B0E mov ecx,dword ptr ds:[esi] ;读取原始esp |
vPop r15
1 | 00451F05 8B0E mov ecx,dword ptr ds:[esi] ;读取原始esp |
vPush ebp(0x19ff74)
1 | 0045325C 0FB60F movzx ecx,byte ptr ds:[edi] ;读取opcode的第一个字节 |
vPush esp(0x0019FF2C)
1 | 004786D0 8BC6 mov eax,esi ;原始esp传给eax,当前esp保存的是变量a的地址 |
vPop ebp(0x19FF2C)
1 | 004343B9 8B0E mov ecx,dword ptr ds:[esi] ;读取原始esp,ecx=保存了变量a的地址 |
vPush ecx(0x0)
1 | 0045225C 0FB60F movzx ecx,byte ptr ds:[edi] ;读取opcode的第一个字节 ; test1_vm.0043D0C3 |
vPush esp
1 | 0047E64B 0FB60F movzx ecx,byte ptr ds:[edi] ;读取opcode的第一个字节 |
vPush 1
1 | 00454C04 8B0F mov ecx,dword ptr ds:[edi] ;从opcode读取4字节 |
vPush ebp(0x0019FF2C)
1 | 0041043E 0FB60F movzx ecx,byte ptr ds:[edi] ;读取opcode的第一个字节 |
vPush -4(0xFFFFFFFC)
变量a地址相对于ebp的相对偏移
1 | 0043E97B 8B0F mov ecx,dword ptr ds:[edi] ;从opcode读取4字节 |
1 | mov eax,dword ptr ds:[v_esp] ;变量a地址相对于ebp的相对偏移 |
计算变量a的地址,ebp-4
1 | 0040423D 8B06 mov eax,dword ptr ds:[esi] ;从原始esp读取变量a地址相对于edx的偏移 |
vPop r12
将偏移弹到R12
1 | 0046FF2C 8B0E mov ecx,dword ptr ds:[esi] ;从原始esp读取变量a地址相对于edx的偏移 |
1 | mov eax,dword ptr ds:[v_esp] ;变量a地址 |
mov dword ptr [ebp-4],1
1 | 0041D673 8B06 mov eax,dword ptr ds:[esi] ;变量a的地址 |
vPush -4(0xFFFFFFFC)
变量a地址相对于ebp的相对偏移
1 | 0045BC24 8B0F mov ecx,dword ptr ds:[edi] ;从opcode读取4字节 |
1 | mov eax,dword ptr ds:[v_esp] ;变量a地址相对于ebp的相对偏移 |
计算变量a的地址,ebp-4
1 | 00435011 8B06 mov eax,dword ptr ds:[esi] ;从原始esp获取变量a地址相对于ebp的相对偏移 |
vPop r11
将偏移弹出
1 | 00453841 8B0E mov ecx,dword ptr ds:[esi] ;从原始esp读取数据 |
1 | mov edx,dword ptr ds:[esi] |
把变量a的值存放栈顶
1 | 00463862 8B16 mov edx,dword ptr ds:[esi] ;从原始esp读取变量a的地址 |
1 | 将变量a的值弹到寄存器R15中,联合上面几句等价于 |
vPop r15
1 | 00430521 8B0E mov ecx,dword ptr ds:[esi] ;取出变量a的值 |
vPush 1
1 | 004693CB 8B0F mov ecx,dword ptr ds:[edi] ;获取1 |
取出R15寄存器的值,然后压栈
vPush 15
1 | 00421D3D 0FB60F movzx ecx,byte ptr ds:[edi] ;opcode获取一字节 |
1 | vAdd dword ptr ds:[v_esp+0x4],R15 ;add eax,1 |
vAdd dword ptr ds:[v_esp+0x4],R15
1 | 004616C3 8B06 mov eax,dword ptr ds:[esi] ;读取R15 |
把保存的EFL弹到R7
vPop r7
1 | 0043BD4C 8B0E mov ecx,dword ptr ds:[esi] ;从v_stack栈顶取出保存的efl |
vPop r4
把上面vAdd的结果弹到R4
1 | 0045E179 8B0E mov ecx,dword ptr ds:[esi] ;从v_stack栈顶取出保存的efl |
vPush r4
R4入栈
1 | 0040EB2E 0FB60F movzx ecx,byte ptr ds:[edi] ;读取一字节的opcode |
vPush R0(0x0019FF2C)
1 | 004184B3 0FB60F movzx ecx,byte ptr ds:[edi] |
vPush -4(0xFFFFFFFC)
变量a地址相对于ebp的相对偏移
1 | 004394F5 8B0F mov ecx,dword ptr ds:[edi] |
1 | mov eax,dword ptr ds:[v_esp] ;变量a地址相对于ebp的相对偏移 |
计算变量a的地址,ebp-4
1 | 0046389F 8B06 mov eax,dword ptr ds:[esi] |
vPop r12
将EFL弹出堆栈
1 | 0046D3FB 8B0E mov ecx,dword ptr ds:[esi] |
1 | mov eax,dword ptr ds:[v_esp] ;变量a地址 |
把计算的结果存回变量a
1 | 00452B62 8B06 mov eax,dword ptr ds:[esi] ;变量a的地址 |
vPush R0(0x0019FF2C)
1 | 004494C2 0FB60F movzx ecx,byte ptr ds:[edi] |
vPush -4(0xFFFFFFFC)
变量a地址相对于ebp的相对偏移
1 | 0044A08D 8B0F mov ecx,dword ptr ds:[edi] |
1 | mov eax,dword ptr ds:[v_esp] ;变量a地址相对于ebp的相对偏移 |
计算变量a的地址,ebp-4
1 | 0042A0F0 8B06 mov eax,dword ptr ds:[esi] |
vPop r15
将EFL弹出
1 | 0046C4CB 8B0E mov ecx,dword ptr ds:[esi] |
1 | mov eax,dword ptr ds:[v_esp] ;变量a地址 |
把变量a的值存放栈顶
1 | 00437854 8B16 mov edx,dword ptr ds:[esi] |
vPop r12
将变量a的值保存到寄存器R12,相当于mov ecx,dword ptr [ebp-4]
1 | 00474E9C 8B0E mov ecx,dword ptr ds:[esi] |
vPush R12
push ecx
1 | 00405372 0FB60F movzx ecx,byte ptr ds:[edi] |
vPush 4020FCh
push “%d\n”
1 | 004357B8 8B0F mov ecx,dword ptr ds:[edi] |
vPush VMEntry
重新进入虚拟机的入口,也就是printf执行完毕后的返回地址
1 | 0043AA20 8B0F mov ecx,dword ptr ds:[edi] |
vPush printf
将printf函数地址压入栈
1 | 0044A38D 8B0F mov ecx,dword ptr ds:[edi] |
vPush R4(2)
1 | 004126A8 0FB60F movzx ecx,byte ptr ds:[edi] |
vPush R5(2)
1 | 0047B785 0FB60F movzx ecx,byte ptr ds:[edi] |
vPush
1 | 0045325C 0FB60F movzx ecx,byte ptr ds:[edi] |
vPush
1 | 0045225C 0FB60F movzx ecx,byte ptr ds:[edi] |
vPush
1 | 0047E64B 0FB60F movzx ecx,byte ptr ds:[edi] |
vPush
1 | 0041043E 0FB60F movzx ecx,byte ptr ds:[edi] |
vPush
1 | 00421D3D 0FB60F movzx ecx,byte ptr ds:[edi] |
vPush
1 | 0040EB2E 0FB60F movzx ecx,byte ptr ds:[edi] |
VMExit
退出虚拟机,执行printf
1 | 00415543 8BE6 mov esp,esi |
printf执行完毕后重新回到入口函数执行
2、防侧漏用苏菲
因为是push指令,需要检查栈是否发生溢出,防止vm_context被真实堆栈覆盖。
VMP堆栈检查
1 | 00462636 8D5424 60 lea edx,dword ptr ss:[esp+0x60] ;edx=v_esp+0x60 |
3、简单提几句
- vmp在初始化时会保存当前环境,将寄存器通过push进行保存,退出虚拟机时恢复寄存器
- 在执行系统函数时,vmp会退出虚拟机,恢复初始环境;函数执行完毕后重新进入虚拟机。
- vmp的虚拟化技术是通过将一条一句进行拆分,然后以push和pop形式进行实现,因此被称为栈虚拟机。
- opcode的第一个字节若为操作数,则该操作数指向为vm_context的其中一个寄存器;若为四字节则为下一个要跳转的handler地址。
- vmp是真的恶心啊。
四、真理只在大炮射程之内
经过对VMP虚拟化代码的分析后,可以使用push和pop指令按照vmp虚拟化的方式对原始的汇编语句继续改写,进行娱乐。
模拟VMP指令
1 |
|
运行无任何问题且能正常输出。
五、精准射击
- https://back.engineering/17/05/2021/[VMProtect 2 - Detailed Analysis of the Virtual Machine Architecture]
- https://back.engineering/17/05/2021/[Quick look around VMP 3.x - Part 1 : Unpacking]
- https://www.52pojie.cn/thread-586130-1-1.html[VMProtect 3.09 虚拟机架构浅析]
- https://bbs.pediy.com/thread-272152.htm[vmp2代码还原分享]
- https://bbs.pediy.com/thread-217588.htm[通过编译优化进行VMP代码还原]
- https://bbs.pediy.com/thread-225262.htm[如何分析虚拟机(1):新手篇VMProtect 1.81 Demo]