0.前言
虚拟机保护技术,是将程序可执行代码转化为自定义的中间操作码(OperationCode),如果操作码是一个字节,一般可以称为字节码(Bytecode)。 划重点:x86的汇编指令是在cpu里执行的,而字节码指令系统是通过解释指令执行的。
1. 原理
一个简单的虚拟机原理的应用如下:
__int64 sub_4005B6()
{
unsigned int v0; // ST04_4
__int64 result; // rax
signed int i; // [rsp+0h] [rbp-10h]
char v3; // [rsp+8h] [rbp-8h]
for ( i = 0; i <= 14999; i += 3 )
{
v0 = byte_6010C0[i];
v3 = byte_6010C0[i + 2];
result = v0;
switch ( v0 )
{
case 1u:
result = byte_6010C0[i + 1];
*(&byte_604B80 + result) += v3;
break;
case 2u:
result = byte_6010C0[i + 1];
*(&byte_604B80 + result) -= v3;
break;
case 3u:
result = byte_6010C0[i + 1];
*(&byte_604B80 + result) ^= v3;
break;
case 4u:
result = byte_6010C0[i + 1];
*(&byte_604B80 + result) *= v3;
break;
case 5u:
result = byte_6010C0[i + 1];
*(&byte_604B80 + result) ^= *(&byte_604B80 + byte_6010C0[i + 2]);
break;
default:
continue;
}
}
return result;
}
byte_604B80是一个数组,部分内容如下:
含有15000个元素的数组,每三个元素为一组,而case1,2,3,4,5相当于是opcode,以他们为索引,进行加减异或等操作,而不是简单的通过,add,xor等汇编指令的实现,当然这是虚拟机保护的原理,个人感觉并不是真正的虚拟机的实现。 典型的比较成熟的虚拟机有Java虚拟机(基于栈)Dalvik虚拟机(基于寄存器)
2.详述
一个虚拟机的整图概述如下
流程是:首先进行虚拟机初始化,然后将字节码通过VMDispatch对字节码进行调度,下面的每一个句柄,相当于cpu中的一条指令,VMDispatch对字节码的内容进行解释后,分发到不同的handler执行。
2.1 VStartVM
将寄存器的符号压入堆栈,一个handler执行完毕后会回到这里,等待继续解释,执行下一个handler,形成一个循环。其中有几个调用约定,一般情况不能将这些寄存器另作他用。
edi=VMcontext
esi=当前字节码的地址
ebp=真实堆栈
2.2 VMContext
这是执行时的虚拟环境结构,与真实环境相对应。
struct VMContext
{
DWARD v_eax;
DWARD v_ebx;
DWARD v_ecx;
DWARD v_edx;
DWARD v_esi;
DWARD v_edi;
DWARD v_ebp;
DWARD v_efl;
//没有esp,为保持堆栈平衡,使程序在虚拟环境解释执行完毕后,能够返回真实环境继续执行,所以esp放入真实环境的ebp里,
}
2.3 VBegin
上面说到了保持堆栈平衡,需要首先设计要一个handler来执行。于是就有了VBegin,执行完这个之后,才能保持堆栈平衡,然后去执行真正的代码。
VBegin:
mov eax,dward ptr [ebp]
mov [edi+0x1c],eax ;对照VMContext得出为,v_efl
add esp,4
mov eax,dward ptr [ebp]
mov [edi+0x18],eax ;对照VMContext得出为,v_ebp
add esp,4
mov eax,dward ptr [ebp]
mov [edi+0x14],eax ;对照VMContext得出为,v_edi
add esp,4
mov eax,dward ptr [ebp]
mov [edi+0x10],eax ;对照VMContext的偏移
add esi,4 ;esi+4,等待分析下一个字节
得出为,v_esi
add esp,4
mov eax,dward ptr [ebp]
mov [edi+0x0c],eax ;对照VMContext得出为,v_edx
add esp,4
mov eax,dward ptr [ebp]
mov [edi+0x08],eax ;对照VMContext得出为,v_ecx
add esp,4
mov eax,dward ptr [ebp]
mov [edi+0x04],eax ;对照VMContext得出为,v_ebx
add esp,4
mov eax,dward ptr [ebp]
mov [edi],eax ;对照VMContext得出为,v_eax
add esp,4
add esp,4 ;释放参数
jmp VMDispatcher ;跳转至调度器
3.虚拟机简单设计
其实说到底,最重要的还是各个handler的设计,而且是那些可以执行基础汇编指令功能的handler的设计,也就是代替x86指令的handler。简单分析几种handler:3.1~3.4为栈操作handler,
3.1 push寄存器的值
vPushReg32:
mov eax,dword ptr [esi] ;从字节码中得到VMContext的偏移
add esi,4 ;esi+4,等待分析下一个字节
mov eax,dword ptr [edi+eax] ;基址+偏移得到寄存器的值
push eax ;压入寄存器
jmp VMDispatcher ;返回调度器,等待下一个handler
3.2 push立即数
vPushImm32:
mov eax,dword ptr [esi] ;从字节码中得到VMContext的偏移
add esi,4 ;esi+4,等待分析下一个字节
push eax ;压入寄存器
jmp VMDispatcher ;返回调度器,等待下一个handler
3.3 push内存地址
vPushMem32:
mov edx,0
mov ecx,0
mov eax,dword ptr [esp] ;第一个寄存器的偏移
test eax,eax
cmovge edx,dword ptr[edi+eax] ;如果不是负数则赋值
mov eax,dword ptr [esp+4] ;第二个寄存器偏移
test eax,eax
cmovge edx,dword ptr[edi+eax] ;如果不是负数则赋值
imul ecx,dword ptr[esp+8] ;第二个寄存器的乘积
add ecx, dword ptr[esp+0x0c] ;第三个内存地址常量
add edx,ecx
add esp,0x10
push edx ;压入寄存器
jmp VMDispatcher ;返回调度器,等待下一个handler
3.4 pop寄存器的值
vPopReg32:
mov eax,dword ptr [esi] ;从字节码中得到VMContext的偏移
add esi,4 ;esi+4,等待分析下一个字节
pop dword ptr[edi+eax] ;弹出寄存器
jmp VMDispatcher ;返回调度器,等待下一个handler
3.5 add
vadd:
mov eax,[esp+4] ;取源操作数
mov ebx,[esp] ;取目的操作数
add ebx,eax ;执行相加操作
add esp,8 ;平衡堆栈
push ebx ;把得到的值压入堆栈
划重点:x86指令集中,add的形式很多,如:add 寄存器,立即数;add 寄存器,寄存器等,操作数可能是寄存器也可能是立即数。在虚拟机中,通过堆栈handler的处理,已经是一个立即数了,执行到vadd时,可以直接使用立即数相加。无论操作数是什么形式,都会经过vadd这一处理步骤,只不过是堆栈处理方式不同,使用了不同的handler。
3.6 条件转移与无条件转移
因为esi指向当前字节码的地址,更改esi的值,就能更改程序的流程。类似于汇编指令执行时的eip指针,始终指向当前程序执行的汇编语句。
3.6.1 无条件转移jmp
vJmp:
mov esi,dword ptr[esp] ;esp指向的值是要跳转的地址
add esp;4 ;移动一个字
jmp VMDispatcher ;返回调度器,等待下一个handler
3.6.2 有条件转移jcc
3.6.2.1 通过cmov模拟跳转
指令比较多,举两个例子参考:
vjne:
cmovne esi,[esp]
add esp,4
jmp VMDispatcher
vjae:
cmovae esi,[esp]
add esp,4
jmp VMDispatcher
划重点:由于在x86指令集中,jcc这类的跳转指令与cmovcc这类的条件传输指令,用有相似的功能,基本上所有的条件跳转都能与条件转移相对应。因此使用条件转移指令模拟。
3.6.2.2 通过判断标志位实现跳转
标志位寄存器如下
vJAE:
push [edi+0x1c] ;
pop eax ;得到标志位
and eax,1 ;和1做and运算,这样就去除了CF位
cmove esi,[esp] ;然后使用判断是否跳转,cmove是等于0时传送
add esp,4 ;移动esp
jmp VMDispatcher ;返回调度器,等待下一个handler
划重点:1的计算是跟据标志寄存器来的,要检测哪一位,就将哪一位置位1,然后转为16进制。
3.7 call指令
vcall:
push all vreg ;所有虚拟寄存器
pop all reg ;弹出到真实的寄存器
push 返回地址
push 要调用的函数的地址
retn
划重点:call指令执行时,不再当前堆栈执行,将控制权将给call调用的函数。在虚拟机保护中,同样的,执行到call时,会跳出虚拟机,让子程序在cpu中执行。要使程序重新返回虚拟机,就需要更改返回地址,重新让虚拟机接管代码。
3.8 retn指令
vRetn:
xor eax,eax
mov ax,word ptr[esi]
add esi,2
mov ebx, dword ptr[ebp] ;得到要返回的地址
add ebp,4 ;释放空间
add ebp,eax ;释放操作数
push ebx ;压入返回地址
push ebp ;压入堆栈指针
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
pop esp ;还原堆栈指针到esp
retn
划重点:retn在虚拟机里是一个退出函数,主要做的操作就是,释放空间,操作数,返回值,将寄存器值赋给真实环境的寄存器。此时这个虚拟机环境也就相当于自动销毁了。
3.9 不可模拟指令
执行方法,先退出虚拟机,执行完指令后,再压入下一个字节码,重新进入虚拟机。
4.总结
虚拟机将汇编指令转换为字节码来模拟执行的,因为汇编指令和字节码值之间的特性不同,使得不能完美的模拟汇编指令,所以这也是虚拟机保护的局限性,无法将直接用汇编写的比较具有技巧性的代码成功转换为字节码执行。