虚拟机保护技术分析
虚拟机(VM)
本文所讨论的虚拟机和VMware等虚拟机化环境不是同一种东西,它是一种基于虚拟机的代码保护技术(Virtual Machine-Based Protection, VMP),准确地说,这里讨论的虚拟机是一种解释执行系统(例如Visual Basic 6 中的PCODE编译方式)。现在的一些动态语言(例如Ruby、Python、Lua和.NET等)从某种角度来说也是解释执行的。
解释执行指的是虚拟机逐条解释并执行字节码指令,而不是将整个程序编译成机器码后再执行。这种方式允许更高的灵活性和安全性,因为代码在运行时才会被解释执行。
Visual Basic 6 中的PCODE(P-Code,也称为Pseudo Code)编译方式是一种中间代码编译方法,用于将Visual Basic代码编译成中间代码,而不是直接生成本机机器代码。这种中间代码可以在运行时由VB6的解释器执行。
Python字节码是Python程序的一种中间表示形式,类似于P-Code或Java字节码。
VMP
虚拟机保护技术(VMP)就是一种将基于X86汇编系统的可执行代码转换为字节码指令系统的代码,以达到保护原有指令不被轻易逆向或篡改的目的。
这种指令执行系统和Intel的x86指令系统不在同一个层次中。例如,80x86汇编指令是在CPU里执行的,而字节码指令系统是通过解释指令执行的(这里的字节码指令系统是建立在x86指令系统上的)。
工作原理:
代码转换:
初始编译:将高层次语言(如C++、C#等)编译成x86汇编代码或中间表示(IR)。
字节码生成:将x86汇编代码转换为虚拟机特定的字节码指令。这个转换过程会混淆和加密指令,生成难以理解的字节码。
虚拟机执行:
解释器:在程序运行时,一个专门的虚拟机解释器负责解释和执行这些字节码指令。
动态执行:解释器逐条读取字节码指令并在底层的x86指令系统上执行相应的操作。
虚拟机执行情况
在上图中,有几个组成部分:
VStartVM部分初始化虚拟机
VMDispatcher部分调度这些Handler。调度执行完后会返回VMDispatcher,形成循环
Bytecode是由指令执行系统定义的一套指令和数据组成的一串数据流。
Handler是一段小程序或者一段过程
整体过程:
虚拟机初始化 (VStartVM)
设置虚拟机的初始状态,包括寄存器、堆栈、内存等。
准备好虚拟机执行所需的环境。
指令调度 (VMDispatcher)
从字节码流中读取下一条指令。
根据指令的操作码(opcode),查找对应的Handler。
将控制权传递给找到的Handler进行具体的指令处理。
指令处理 (Handler)
每个Handler处理特定的字节码指令,执行相应的操作。
Handler执行完毕后,将控制权返回给VMDispatcher。
循环执行
VMDispatcher继续读取下一条字节码指令,重复调度和处理的过程,直到程序结束。
在整个过程中,Bytecode(字节码)相当于在真实CPU中运行的二进制代码,包含程序的所有指令和数据,VMDispatcher类似于CPU的指令调度器,负责取指令、解释指令,并将其发送给对应的执行单元(Handler),Handler(指令处理程序)就是CPU指令的执行单元,每个Handler对应于虚拟机指令集中的一条指令,并执行具体操作。
VMContext
**“VMContext”**结构体用于存储虚拟机执行环境中的各个虚拟寄存器状态。这个结构体包含了需要在虚拟机指令执行过程中保存和操作的所有重要寄存器。
struct VMContext
{
DWORD v_eax;
DWORD v_ebx;
DWORD v_ecx;
DWORD v_edx;
DWORD v_esi;
DWORD v_edi;
DWORD v_ebp;
DWORD v_efl; // 符号寄存器(虚拟 EFLAGS)
};
调用约定
在虚拟机保护技术中,特定的寄存器约定对于虚拟机的执行过程非常重要,它们确保了虚拟机在执行过程中能够正确访问和处理关键的数据结构和指令流。
如上述虚拟机执行流程:
虚拟机启动调用VStartVM,初始化虚拟机上下文和堆栈,保存当前寄存器状态。
加载字节码地址到 ESI,设置虚拟机栈和上下文地址。
VMDispatcher 从 ESI 读取操作码,根据操作码从 JUMPADDR 表中查找处理程序地址。
根据操作码的定义,处理程序执行具体的指令操作,如寄存器运算、内存访问等。
返回 VMDispatcher,继续下一个字节码指令的调度和执行。
代码如下:
section .data
; 定义 JUMPADDR 表,每个条目存储一个处理程序的地址
JUMPADDR dd Handler0, Handler1, Handler2, ... ;
section .text
global VStartVM
VStartVM:
pusha ; 保存所有通用寄存器的状态到堆栈
push ebx
push ecx
push edx
push esi
push edi
push ebp
pushfd
; 设置虚拟机寄存器和堆栈
mov esi, [esp + 0x20] ; 将堆栈上的值(字节码地址)加载到 ESI
mov ebp, esp ; 设置 EBP 为当前 ESP(虚拟机栈)
sub esp, 0x200 ; 为虚拟机栈分配空间
mov edi, esp ; 设置 EDI 为新的 ESP(虚拟机上下文)
; 进入虚拟机调度循环
jmp VMDispatcher
VMDispatcher:
mov al, byte ptr [esi] ; 从 ESI 指向的字节码地址处读取一个字节(操作码)
inc esi ; 增加 ESI,使其指向下一个字节
movzx eax, al ; 将 AL 扩展为 EAX
mov eax, dword ptr [eax * 4 + JUMPADDR] ; 根据操作码从 JUMPADDR 表中获取处理程序的地址
jmp eax ; 跳转到处理程序地址执行
从中可以如下约定:
edi 指向 VMContext 的起始值
edi
寄存器在虚拟机执行过程中始终指向 VMContext 结构体的内存地址。通过这个地址,虚拟机可以方便地访问和修改虚拟寄存器的状态。
esi 指向字节码的地址
esi
寄存器指向包含字节码指令的内存区域。虚拟机解释器通过esi
逐条读取字节码指令,并根据指令操作码执行相应的 Handler。
ebp 指向 VM 栈地址
ebp
寄存器在虚拟机执行过程中用于指向虚拟机栈的基址。虚拟机栈用于存储局部变量、函数参数和返回地址等。
Handler设计
VMP中的Handler并不是Windows中的句柄,而是一小段程序或者一段过程,它由VMP中的VMDispatcher调度。每个Handler对应一种字节码指令,负责执行特定的操作。
Handler又分为俩大类,一类是辅助Handler,另一类是普通Handler。辅助Handler用于执行一些重要的、基本的指令,通常与虚拟机的核心功能和上下文管理有关,比如push和pop等处理栈的Handler;普通Handler用于执行常规的x86指令,比如一些算术运算、逻辑运算或者是比较和分支。
辅助Handler
Push:
; 获取要压入栈的值,假设值在 eax 中
sub ebp, 4 ; 栈指针向下移动,预留出4个字节的空间
mov [ebp], eax ; 将 eax 中的值压入栈顶
jmp VMDispatcher ; 返回调度器
Pop:
; 将栈顶的值弹出到 eax 中
mov eax, [ebp] ; 从栈顶获取值到 eax
add ebp, 4 ; 栈指针向上移动,释放4个字节的空间
jmp VMDispatcher ; 返回调度器
普通Handler
Add:
mov eax, [edi + 0] ; 从虚拟寄存器 v_eax 中加载值到 eax
mov ebx, [edi + 4] ; 从虚拟寄存器 v_ebx 中加载值到 ebx
add eax, ebx ; 执行加法运算
mov [edi + 0], eax ; 将结果存回虚拟寄存器 v_eax
jmp VMDispatcher ; 返回调度器
Sub:
mov eax, [edi + 0]
mov ebx, [edi + 4]
sub eax, ebx
mov [edi + 0], eax
jmp VMDispatcher
And:
mov eax, [edi + 0]
mov ebx, [edi + 4]
and eax, ebx
mov [edi + 0], eax
jmp VMDispatcher
Mov:
mov eax, [esi] ; 读源寄存器索引
mov ebx, [esi + 1] ; 读目标寄存器索引
mov ecx, [edi + eax * 4] ; 从源虚拟寄存器加载值到 ecx
mov [edi + ebx * 4], ecx ; 将值存到目标虚拟寄存器
add esi, 2 ; 跳过字节码中的源和目标寄存器索引
jmp VMDispatcher
标志位处理
VStcHandler:
pushfd ; 保存当前 EFLAGS 到栈中
pop eax ; 将 EFLAGS 存入 eax
mov [edi + 0x1C], eax ; 保存 EFLAGS 到虚拟机上下文的 v_efl
stc ; 设置进位标志(CF)
pushfd ; 将修改后的 EFLAGS 保存到栈中
pop eax ; 将修改后的 EFLAGS 存入 eax
mov [edi + 0x1C], eax ; 更新虚拟机上下文中的 v_efl
jmp VMDispatcher ; 返回调度器
在x86中,涉及标志位运算的指令很多,有的是设置标志位,有的是判断标志位,所以应该在相关Handler前保存标志位,在相关Handler后恢复标志位。如上,stc将标志的CF位置为1。
转移指令
转移指令包括条件转移、无条件转移、call和retn。
在实现转移指令时,可以将esi
指向当前字节码的地址,esi指令就像真实CPU中的eip
寄存器,可以通过改写esi
寄存器的值来更改流程。
无条件跳转指令jmp
的Handler如下:
vJmp:
mov esi, dword ptr [esp] ; [esp]指向要跳转的地方
add esp, 4 ; 弹出栈顶地址
jmp VMDispatcher ;
在实现一些条件转移指令,如ja
、je
时,因为它们要根据标志位来判断流程,略显麻烦,可以使用条件传输指令替代。
条件转移指令 | 条件传输指令 |
---|---|
ja | cmova |
jae | cmovae |