虚拟机保护技术
虚拟机保护是一种基于虚拟机的代码保护技术,其将可执行代码转化为字节码(vm_code),并通过自己的指令执行解释系统对这些字节码进行解释并执行专门的子程序(handler)。这个用来解释字节码并执行子程序的系统称为“虚拟机”或“调度器”(可以理解为是一个虚拟的CPU),其类似于JAVA的虚拟机JVM,其他的解释型语言也都有类似的解释系统。因为虚拟机保护的代码经过了加工处理后极难分析出程序真正的流程,所以虚拟机保护技术的安全性较强。
虚拟机保护的流程
虚拟机保护是基于栈的,虚拟机保护的代码的运行实际就是对栈的各种复杂操作。
int main(int argc,char argv[])
{
A();
return 0;
}
VStartVM
当函数A的汇编指令被虚拟机保护编译器编译后,假设在调用函数A前栈的状态如下图。
函数A被使用了虚拟机保护,调用函数A的代码会比变为如下形式。其中VMStartVM进行虚拟机初始化,而参数vm_codeaddress是当前字节码的地址。每当想进入虚拟机时都是使用此代码。
push vm_codeaddress
jmp VMStartVM
VMStartVM是对虚拟机进行初始化,其代码如下。其先将各个寄存器的值保存在栈中,然后又在栈中开辟了0xC8个空间用来存放VMContext虚拟环境结构(虚拟的寄存器)。最后edi指向VMContext,ebp指向虚拟的栈顶,esi指向当前字节码的地址,在虚拟机运行过程中不能随意改变这几个寄存器的值。
VStartVM:
push eax
push ebx
push ecx
push edx
push esi
push edi
push ebp
pushfd
mov esi ,[esp+0x20] ;esp+0x20指向VStartVM的参数,即当前字节码vm_code的地址
mov ebp,esp ;ebp就是虚拟的栈顶
sub esp,0xC8 ;在堆栈中开辟0xC8字节(当然也可以多开辟点)存放VMcontext
mov edi,esp ;edi指向VMcontext
VMDispatcher:
movzx eax,byte ptr[esi] ;获得字节码vm_code
lea esi,[esi+1] ;指向下一个字节码
Jmp dword ptr [eax*4+JumpAddr] ;跳到Handler执行处,
当执行完VStartVM后栈的情况如下图所。
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;
}
平衡栈vBegin 和 vCheckSTACK
当初始化完成完成后我们要平衡堆栈,vBegin这个handler是用来平衡堆栈的。
vBegin:
mov eax,dword ptr [ebp]
mov [edi+0x1C],eax ;v_efl
add ebp,4
mov eax,dword ptr [ebp]
mov [edi+0x18],eax ;v_ebp
add ebp,4
mov eax,dword ptr [ebp]
mov [edi+0x14],eax ;v_edi
add ebp,4
mov eax,dword ptr [ebp]
mov [edi+0x10],eax ;v_esi
add ebp,4
mov eax,dword ptr [ebp]
mov [edi+0x0C],eax ;v_edx
add ebp,4
mov eax,dword ptr [ebp]
mov [edi+0x08],eax ;v_ecx
add ebp,4
mov eax,dword ptr [ebp]
mov [edi+0x04],eax ;v_ebx
add ebp,4
mov eax,dword ptr [ebp]
mov [edi],eax ;v_eax
add ebp,4
jmp VMDispatcher
vBegin将刚刚在初始化虚拟机保存的寄存器的值都复制到edi指向的VMContext中对应的虚拟寄存器中,这样ebp就指向了参数的地址。其堆栈情况如下:
其中可供虚拟机使用的虚拟堆栈有(0XC8 - 0X20) + 0X20 + 4, 如果使用的栈空间大于这些的话就会把edi指向的VMContext给覆盖掉。为了防止被覆盖将会在往虚拟栈中压入数据时检查栈顶是否会覆盖VMContext,如果会就会将VMContext结构复制到栈的更远处。这正是vCheckSTACK这个handler完成的工作。这些都完成后就可以执行代码了。
handler设计
handler主要分辅助handler和普通handler。辅助handler主要完成堆栈的操作,而普通handler利用辅助handler设置好的堆栈执行相关指令。
例如vPushReg32是一个辅助handler,其功能是将32位寄存器压栈。其字节码为寄存器在VMContext结构中的偏移值
vPushReg32:
Mov eax,dword ptr[esi] ;获得VMcontext结构中的偏移地址
inc esi,4
mov eax,dowrd ptr [edi +eax] ;得到寄存器的值。edi指向VMcontext结构的基址
sub ebp,4
mov dword ptr [ebp],eax ;压入虚拟堆栈
例如vadd是一个普通handler,其用于执行两个操作数的加法。其将虚拟栈顶的两个单元当做两个操作数,进行相加后结果保存在虚拟栈顶。
vadd:
mov eax,[ebp+4]
mov ebx,[ebp]
add ebx,eax
mov [ebp],ebx
由上述指令我们可以得到add esi,eax的指令可以转化为如下代码
vPushReg32 eax_index
vPushReg32 esi_index
vadd
vPopReg32 esi_index
CALL指令
因为虚拟机保护是基于栈的,如果被虚拟化保护的函数中又调用了其他函数,也就是执行call指令就需要先退出虚拟机,然后调用完函数后再次进入虚拟机接着下一个字节码执行。(不可模拟的指令和CALL一样,先退出虚拟机,然后执行指令,在进入虚拟机)
vcall:
push all vreg ;所有虚拟寄存器(VMContext)
pop all reg ;所有真是的寄存器
mov esp,ebp ;恢复esp
add esp,4 ;释放参数,相当于退出了虚拟机
push 返回地址
push 要调用的函数的地址 ;调用需要call的函数
retn
注意返回地址处应为如下指令,用来在调用完call函数后再次进入虚拟机。
vtheNextVM:
push 紧接着的字节码地址
jmp VStartVM
当执行vcall时堆栈发生了如下变化:
retn指令
当我们执行完A函数的所有指令后(虚拟化保护函数的所有指令)需要返回到A函数外部。
我们先将返回地址放到[ebp]中,接着执行vRetn将esp恢复后,将edi指向的VMContext(虚拟寄存器)的值还原到真是寄存器中。然后就执行retn返回到函数外部。这里注意我们不需要清除在进入虚拟机VStartVM时压入栈的参数(字节码的地址),因为其已经在平衡堆栈vBegin中释放了。
vRetn:
mov esp,ebp ;先将返回地址放到[ebp],再恢复esp
Push [edi + 0x1c]
Push [edi + 0x18]
Push [edi + 0x14]
Push [edi + 0x10]
Push [edi + 0x0c]
Push [edi + 0x08]
Push [edi + 0x04]
Push [edi]
pop eax
pop ebx
pop ecx
pop edx
pop esi
pop edi
pop ebp
Popfd
sub esp,4 ;释放参数,恢复堆栈
retn
最后其堆栈又恢复到进入虚拟机前的状态
以上代码参考看雪论坛《加密与解密》第四版并结合自己的理解。