参考文章:看雪虚拟机保护技术浅谈,人肉跟踪VMProtect入口
虚拟机概览
所谓虚拟机保护技术,是指将代码翻译为机器和人都无法识别的一串伪代码字节流(字节码);在具体执行时再对这些伪代码进行一一翻译解释,逐步还原为原始代码并执行。字节码它是由指令执行系统定义的一套指令和数据组成的一串数据流。
负责用于翻译伪代码并负责具体执行的子程序就叫做虚拟机VM,好似一个抽象的CPU。它以一个函数的形式存在,函数的参数就是字节码的内存地址。
这张图是一个虚拟机执行时的整图概述,VStartVM部分初始化虚拟机,VMDispatcher负责调度这些Handler,Handler可以理解为一个个的子函数(功能代码),它是每一个伪指令对应的执行功能代码.
为什么要出现一条伪指令对应着一个Handler执行模块呢?
这和虚拟机加壳的指令膨胀有关,被虚拟机加壳后,同样一条指令被翻译成了虚拟伪指令,
一条虚拟伪指令往往对应着好几倍的等效代码,当中可能还加入了花指令,整个Handler加起来可能就等效为原本的一条x86汇编指令。
Bytecode就是虚拟伪指令,在程序中,VMDispatcher往往是一个类while结构,不断的循环读取伪指令,然后执行。
虚拟机架构
虚拟机不可能针对每一种具体情况都进行翻译处理。必须对所有可能遇到的指令先进行抽象归类,然后分解为若干简单的小指令,再交由各个专门的子程序(handler)去处理。
三元式代码(3地址代码)
即不论多么复杂的赋值公式,都可以分解为数个3地址代码式序列。1段3地址代码只完成1次运算,譬如1次二目运算、1次比较,或者1次分支跳转运算。论多么复杂的指令,都可以分解为一串不可再分割的原子指令序列。
虚拟机(CPU)的体系架构可分为3种,基于堆栈的(Stack based),基于寄存器的(Register based)和3地址机器。基于堆栈的虚拟机体系架构需要频繁操作堆栈,其使用的虚拟寄存器保存在堆栈中,每个原子指令的handler都需要push,pop。譬如指令add,基于堆栈的CPU首先从堆栈里Pop两个数,然后将两数相加,再把和Push到堆栈。Add指令只占用1个字节。而基于寄存器的CPU对应指令为 add Reg1,Reg2,需要3个字节。
虚拟机保护技术,就是把基于寄存器的CPU代码,改造成基于堆栈的CPU的伪代码。然后再由基于堆栈的虚拟机(CPU)对伪代码解释执行。
VStartVM是虚拟机的入口,负责保存运行环境(各个寄存器的值)、以及初始化堆栈(虚拟机使用的变量全部在堆栈中)。Bytecode是伪代码;VMDispatcher对伪代码逐个阅读处理,然后分发给下面的各个子程序(Handler)。 加壳程序先把已知的X86指令解释成了字节码,放在PE文件中,然后将原处代码删掉,改成类似的代码进入虚拟机执行循环。
1 | push bytecode ;伪代码地址 ,作为参数 |
初始化后堆栈如图 :edi指向VMcontext;esi指向伪代码的地址;ebp指向真实堆栈的栈顶; 这三个寄存器在VM内不要再改了。
VMContext是虚拟机VM使用的虚拟环境结构:
1 | Struct VMContext |
用简单的add指令作为示例
1 | Vadd: ;virtual add |
上图是参考文章中的图,和我自己画的那个差不多,区分了颜色来说一下蓝-橙-黄部分作用(一条vmp指令的执行)。
* 蓝色:加载并解密字节码。
- 橙色:根据字节码数值计算出handler地址。
- 黄色:执行该handler逻辑。
前十几个蓝橙黄组合都是一种handler,执行的是一种vPop操作,如上图箭头,是将真实寄存器的值移动到VMcontext
区域,一个vPop移动一个值。
后十几个蓝橙黄组合
最后一个vRet,将压入vmp运算栈中vmp虚拟寄存器值,更新到真实cpu寄存器中,至此,退出虚拟机。