虚拟机保护技术浅谈

对加密与解密的内容进行了一些总结、重新归纳整理,加入了自己的理解;希望对新手有所帮助。

《加密与解密》第三版  第471页

虚拟机保护技术

虚拟机概览

所谓虚拟机保护技术,是指将代码翻译为机器和人都无法识别的一串伪代码字节流;在具体执行时再对这些伪代码进行一一翻译解释,逐步还原为原始代码并执行。

这段用于翻译伪代码并负责具体执行的子程序就叫作虚拟机VM(好似一个抽象的CPU)。它以一个函数的形式存在,函数的参数就是字节码的内存地址。

将虚拟机应用到商业中的保护壳现有三款:Vmprotect,themida和 execrypt。

虚拟机架构

我们知道,代码中的指令多种多样,组织形式也千变万化;虚拟机不可能针对每一种具体情况都进行翻译处理。必须对所有可能遇到的指令先进行抽象归类,然后分解为若干简单的小指令,再交由各个专门的子程序(handler)去处理。

学过编译原理的同学应该都知道三元式代码吧,也叫做3地址代码(three

adress

code)。即不论多么复杂的赋值公式,都可以分解为数个3地址代码式序列。(什么是3地址代码,1段3地址代码只完成1次运算,譬如1次二目运算、1次比较,或者1次分支跳转运算。)

与此类似,不论多么复杂的指令,都可以分解为一串不可再分割的原子指令序列。

虚拟机(CPU)的体系架构可分为3种,基于堆栈的(Stack based),基于寄存器的(Register

based)和3地址机器。我们只讲述基于堆栈的虚拟机架构(Stack based);这种架构的虚拟机需要频繁操作堆栈,其使用的虚拟寄存器(虚拟的eax、ebx等等)保存在堆栈中;每个原子指令的handler都需要push、pop。

现在的CPU都有大量的寄存器,堆栈一般只是在函数传递参数时使用(譬如PC机用的x86系列CPU)。但也有一些CPU只操作内存,没有堆栈,也没有寄存器。使用这种CPU的机器称之为3地址机器。

基于堆栈的CPU或虚拟机没有临时变量、寄存器的概念,所有的东西都放入堆栈。由于指令中不需要指定操作数,所以其指令相对基于寄存器的要短。也因此相对简单,在嵌入式系统中运用较多。用于保护代码,我们也选择这种。

举例,譬如指令add,基于堆栈的CPU首先从堆栈里Pop两个数,然后将两数相加,再把和Push到堆栈。Add指令只占用1个字节。而基于寄存器的CPU对应指令为

add

Reg1,Reg2,需要3个字节。请仔细想象一下没有寄存器的CPU,它的指令是怎样的,该会是多么简洁。当然,指令简洁带来的缺点就是效率低下。

我们这里谈的虚拟机保护技术,就是把基于寄存器的CPU代码,改造成基于堆栈的CPU的伪代码。然后再由基于堆栈的虚拟机(CPU)对伪代码解释执行。

指令系统

关键在于设计一个虚拟的基于堆栈的虚拟机(CPU)的指令系统。这个指令系统越简洁,复用性越高越好。

还是以add

指令为例。X86系列CPU的add指令有许多格式,譬如:add reg,imm 、add reg,reg、add reg,mem、add

mem,reg等等。而基于堆栈的虚拟机CPU则没有这么多花样,就一个单单的add指令,参数和返回都是在堆栈里。

我们需要为我们的虚拟机CPU模拟实现这样的add命令:

Vadd:                            ;virtual add        Mov eax,[esp+4]          ;取源操作数        Mov ebx,[esp]            ;取目的操作数        Add ebx,eax              ;        Add esp,8                ;把参数从堆栈中删掉,平衡堆栈        Push ebx                ;把结果压入堆栈

而原有的add命令的参数,我们需要翻译为 push 命令 。根据push 的对象不同,需要不同的实现:

vPushReg32:                      ;寄存器入栈 。esi指向字节码的内存地址      Mov eax,dword ptr[esi]    ;从伪代码(字节码)中获得寄存器在VMcontext结构中的偏移地址      Add esi,4                  ;VMcontext结构保存了各个寄存器的值。该结构保存在堆栈内。      Mov eax,dowrd ptr [edi +eax]  ;得到寄存器的值。edi指向VMcontext结构的基址      Push eax                    ;压入堆栈      Jmp VMDispatcher            ;任务完成,跳回任务分派点vPushImm32:                      ;立即数入栈      Mov eax,dword ptr[esi]      ;字节码,不用翻译就是了      Add esi,4      Push eax                    ;立即数入栈      Jmp VMDispatcher

有Push指令了,也得有Pop指令:

vPopReg32:      Mov eax,dword,ptr[esi]    ;从伪代码(字节码)中获得寄存器在VMcontext结构中的偏移地址      Add esi,4      Pop dword ptr [edi+eax]    ;弹回寄存器      Jmp VMDispatcher

基于堆栈的虚拟机指令系统就是这样简单的:单字节的动作指令(譬如add、dec),以及各式各样的push、pop等堆栈操作指令。没有复杂的寄存器与内存操作。我们需要把x86的CPU指令翻译成虚拟机CPU的指令,譬如:

Add esi,eax        转换为虚拟机的指令如下:          vPushReg32 eax_index          vPushReg32 esi_index          Vadd          vPopReg32 esi_index      ;不弹eax_index,它作为返回结果保存在堆栈里

涉及跳转的指令:(譬如Jmp、Call、Ret)

Esi指向当前伪代码的地址,就好比我们虚拟机CPU的IP寄存器,指向当前指令的地址。

可以通过修改Esi来改变程序执行的流程。譬如:

Vjmp:    Mov esi,dword ptr [esp]  ;jmp的参数在堆栈中    Add esp,4                ;平衡堆栈    Jmp VMDispatcher

Call指令相对麻烦一点,因为Call的函数未必是虚拟机的伪代码了。所以碰到Call指令,就要退出虚拟机,交由真实的CPU去处理了。代码类似下面:

Vcall:    Push all vreg            ;所有的虚拟的寄存器 (维护在堆栈中)    Pop  all reg            ;弹出到真实的寄存器中(保存虚拟机的运行结果)    Push 返回地址            ;可以让Call调用完成后,把控制权归还给虚拟机。详细见原书第480页。    Push 要调用的函数地址    Retn

以下是vRetn:

vRetn:    Xor eax,eax    Mov ax,word ptr [esi]    ;retn的操作数是WORD类型的    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                  ;还原堆栈指针    retn

其余,要注意标志位的处理、不可模拟指令,以及指令的优化。还有异常处理,这里就不展开了。

VStartVM是虚拟机的入口,负责保存运行环境(各个寄存器的值)、以及初始化堆栈(虚拟机使用的变量全部在堆栈中)。

Bytecode是伪代码;VMDispatcher对伪代码逐个阅读处理,然后分发给下面的各个子程序(Handler)。

加壳程序先把已知的X86指令解释成了字节码,放在PE文件中,然后将原处代码删掉,改成类似的代码进入虚拟机执行循环。

push bytecode        ;伪代码地址 ,作为参数

jmp  VstartVM

VstartVM就是进入虚拟机的函数,它的代码大概类似这样的:

VStartVM:  push eax  ;将寄存器压入堆栈,后面会由伪指令取出并存放到VMContext中;此处也可以加入一些随机特征用于迷惑  push ebx  push ecx  push edx  push esi  push edi  push ebp  pushfd  mov esi ,[esp+0x20]  ;esp+0x20指向VStartVM的参数,即伪代码的内存地址  mov ebp,esp          ;ebp指向当前堆栈  Sub esp,0x200        ;在堆栈中开辟0x200字节,存放VMcontext  mov edi,esp          ;edi指向VMcontext  Sub esp,0x40          ;到这里,才到达VM真正使用的堆栈,不一定非要0x40字节VMDispatcher:        Mov eax,byte ptr[esi]  ;获得伪代码 bytecode        Lea esi,[esi+1]                Jmp dword ptr [eax*4+JumpAddr];跳到Handler执行处,由加壳引擎填充                                      ;每读一个byte就跳到函数表模拟执行代码。(堆栈型CPU的指令短,1字节足够)                                        ;JUMPADDR就是一张函数表(有点类似VTBL或者switch-case表),VM_END                            ;VM结束标记

VStartVM初始化后,堆栈情形如下:

堆栈低地址...  ... --------  ...    |  ... 0x40字节的空间,VM使用的堆栈空间(不一定非要0x40大小)  ...    |  ... --------                              edi指向这里(VMcontext)  ...    |  ...  0x200字节的空间,用于存放VMcontext  ...    |  ... --------                                flags  保存的标志寄存器                    ebp指向这里  ebp --------  edi    |  esi    |  edx    保存的寄存器  ecx    |  ebx    |  eax --------  伪代码开始地址(虚拟机参数)                  esi指向这里(伪代码地址)  ...  堆栈高地址...

edi指向VMcontext;esi指向伪代码的地址;ebp指向真实堆栈的栈顶; 这三个寄存器在VM内不要再改了。

VMContext是虚拟机VM使用的虚拟环境结构:

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;        }

VM之所以使用堆栈保存自己的寄存器结构,是考虑到多线程程序的兼容。

大家都知道脱壳时的堆栈平衡原理吧。同样的,虚拟机在执行翻译后的程序代码时,也不可以随便变动原来的堆栈地址。还需经常检查在堆栈中的VMcontext结构不被冲掉。

VCheckESP:        Lea eax,dword ptr [edi+0x100]        Cmp edi,ebp                    ;比较 ebp与edi(VMContext)之间的距离        Jl VMDispatcher                ;如果小于就继续执行        Mov edx,edi        Mov ecx,esp        Sub ecx,edx                    ;ecx = esp - edi        Push esi                        ;保存esi        Mov esi,esp        Sub esp,0x60        Mov edi,esp                    ;edi = esp - 0x60        Push edi                        ;保存新的edi地址        Sub esp,0x40        Cld        Rep movsb                        ;复制        Pop edi        Pop esi    Jmp VMDispatcher

本文由看雪论坛 东方荣克 原创,转载请注明来自看雪社区

你可能感兴趣的:(虚拟机保护技术浅谈)