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

                                  《加密与解密》第三版  第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