什么是虚拟化保护技术

CPU 只认识自己的机器指令(x86/x64 的 opcode 操作码)。
编译:C/C++ → 汇编 → x86/x64机器码(二进制 opcode)
我们正常的计算机执行程序:CPU 直接读取内存里的机器码,按硬件电路直接译码、交给CPU内部的电路完成这条指令背后的操作。

程序指令放在内存中,CPU设置了一个指令寄存器EIP指向下一条要执行的指令,CPU通过这个EIP不断从内存中读取指令,然后译码,交给内部的功能单元完成这条指令。
在这个过程中,有时候需要临时存储数据,CPU设置了一些寄存器可以派上用场,同时还在内存中划一块区域用来记录程序的调用关系以及一些中间数据,这就是栈。
以上就是x86CPU工作的一个简单描述。

为了在软件层面模拟CPU执行二进制的代码指令,需要有以下关键点:

  1. 一个指令调度器,用于分发指令的执行
  2. 一个编译器,将原始CPU指令,转化为虚拟机指令
  3. 一套虚拟机的指令。x86架构下,CPU一旦读取到0x55指令,就知道执行push ebp;一旦读取到0x8BEC,就知道执行mov ebp,esp; 同理,虚拟机也需要有自己的指令集,虚拟CPU才知道自己要干啥。
  4. 每条指令的软件模拟实现,成为Handle
  5. 一个指令寄存器,类似EIP,指向下一条要执行的虚拟指令
  6. 虚拟机的寄存器,用来存放各种临时数据
  7. 虚拟机的堆栈,用来做各种数据交换
    在这个虚拟机中,调度器不断从虚拟的指令寄存器中读取将要执行的指令,读取后,还要将虚拟指令寄存器的指向往后移动,然后根据读取到的指令,调用对应的Handler进行执行。
    这就是一个虚拟机最简单的模型。

虚拟化保护技术就是分析我们要保护的程序,把它里面的CPU指令,替换成虚拟机指令集中的指令,然后再在这个程序中塞入一个虚拟机,通过这个虚拟机来模拟执行这些替换后的虚拟指令。
这样一来,目标程序中不再有真实的物理CPU指令,全是虚拟的指令,在传统的反汇编工具看来,全都是乱码,无法分析。而且它还不同于传统的加壳,会在内存中有解密的过程,虚拟机是没有这个过程的,所以逆向分析的难度陡增。

vmp的虚拟化

我们先了解vmp的几个概念
VM_CONTEXT:VMP在内存开辟一段连续的区域当成虚拟机的寄存器;
VM_DATA:VMP的虚拟指令,不同版本VMP的指令是不一样的,这样可以在一定程度上防止VMP本身被破解;
vEIP:VMP的指令寄存器。

要想更真实具体的去学习vmp的虚拟化,就要自己动手去调试vmp壳,
找到泄露的VMP3.5.1源码编译,这次的重点是虚拟化,我们可以修改源码的一处地方去进行去混淆,去除垃圾指令,
intel.cc文件中,找到Mutate函数,

在函数开头直接加return返回.
在编译好的项目中加载Vmprotect.exe,选择好要保护的程序和代码区段,


选择Virtualization虚拟化,其余功能都选No


这样就获得一个”纯净版”虚拟化保护程序了,更方便我们去进行分析学习VMP虚拟化。

VMEntry:虚拟机初始化


push xxxx,call xxxx这是非常明显的VMP3.xx版本的壳特征,push指令是压入了虚拟指令集的起始地址(已加密的,后续有解密操作)
F7单步运行进入这里:

前面的7条指令是在把通用寄存器压栈,第8条指令pushfd是在把标志寄存器压栈,这里是为了报错原始物理寄存器的值。后续两条指令将0压栈,占位的作用。
这些push指令执行完后,MOV esp+0x28处的值赋给了ebp,ebp经过下面几行指令的解密操作,经过add ebp,edx后,得到真正的VM_DATA地址,这时ebp就是vEIP。

经过上面的操作后,栈如下图所示:

继续往下:有一条关键指令:sub esp,0xC0; 这里把esp往上开辟192byte的空间,作为VM_CONTEXT(也就是虚拟存器)。后续会用esp+ecx来读取这些虚拟寄存器;此时堆栈图如右边:下面是虚拟栈,保存着物理寄存器,上面是VM_CONTEXT,中间esi隔开;

接下来到下一条关键指令:mov ecx,dword ptr ss:[ebp],取出vEIP指向的虚拟字节码赋值给ecx,
add ebp, 4 这就是虚拟EIP的推进——相当于原生CPU执行完一条指令后EIP += 指令长度
后续指令解密字节码,运行到add edi, ecx,计算出的edi就是真正的Handler入口,
jump到Handler函数,开始执行第一条虚拟指令的解释。

指令调度器

在早期的VMP(特别是1.x~2.x版本)中,Dispatcher(分发器)是虚拟机的心脏和大脑——它是一个永不停止的循环解释器,负责从虚拟字节码流中读取指令,解密出操作码,然后跳转到对应的Handler去执行。
早期VMP的Dispatcher的大概逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
vm_dispatcher:
; 1. 从虚拟EIP指向的内存读取加密的操作码
movzx eax, byte ptr [vEIP] ;【取指】从vEIP读取1字节opcode
lea esi, [vEIP+1] ;虚拟指令指针+1,取下一条字节码

; 2. 解密操作码(XOR/ADD/ROL多轮变形)
xor eax, [encryption_key]
sub eax, 0x12345678
rol eax, 0x0B
add eax, 0x9ABCDEF0

; 3. 用解密后的操作码作为索引,跳转到Handler表
mov ebx, [handler_table_base] ; Handler表基址
jmp dword ptr [ebx + eax*4] ; 跳转到对应的Handler

Dispatcher并不是一个独立的循环函数,而是被分散嵌入在Handler的尾部。

1
2
3
4
5
6
7
8
9
10
11
Handler_A:
; 模拟一条x86指令(如add eax, ebx)
... 具体模拟代码 ...
; Handler尾部:跳转回Dispatcher
jmp vm_dispatcher

Handler_B:
; 模拟另一条指令(如push ecx)
... 具体模拟代码 ...
; Handler尾部:跳转回Dispatcher
jmp vm_dispatcher

不过3.x版本的VMP去掉了统一的dispatcher,由上个handler直接跳转到下个handler,有点去中心化的意思。
让我们来看看上面跳转的第一条handle,

这条handle的操作是:
从vEIP指向的虚拟字节码中读出1 字节加密后的VM_CONTEXT索引,放到cl;
一整套变换xor‑not‑add‑rol‑dec‑xor做解密,得到真实索引ecx;
将保存的物理寄存器弹出,放入esp+ecx指向的虚拟寄存器中。

执行完一条handle后,取出vEIP指向的2字节虚拟指令,解密,跳转到下一条handle。
后续几条handle也是这一条,但是ecx是变化的,目的是将虚拟栈中保存的真实寄存器的值映射到相应的虚拟寄存器,完成虚拟寄存器的初始化。

虚拟机堆栈检查机制


这个Handler就是检查当前的虚拟栈栈顶ESI和ESP指向VMContext之间的安全距离是否足够,如果小于安全距离,就继续抬高栈顶,把VM_Context结构复制到更高的位置去。

对于虚拟机里面所有可能涉及修改栈操作的Handler,在执行后都要跳转到这个Check Handler。