0%

虚拟机保护技术

参考文章:看雪虚拟机保护技术浅谈人肉跟踪VMProtect入口

虚拟机概览

​ 所谓虚拟机保护技术,是指将代码翻译为机器和人都无法识别的一串伪代码字节流(字节码);在具体执行时再对这些伪代码进行一一翻译解释,逐步还原为原始代码并执行。字节码它是由指令执行系统定义的一套指令和数据组成的一串数据流。

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

image-20220322164833574

​ 这张图是一个虚拟机执行时的整图概述,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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
push bytecode         ;伪代码地址 ,作为参数
jmp 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结束标记

image-20220322150225897

​ 初始化后堆栈如图 :edi指向VMcontext;esi指向伪代码的地址;ebp指向真实堆栈的栈顶; 这三个寄存器在VM内不要再改了。
VMContext是虚拟机VM使用的虚拟环境结构:

1
2
3
4
5
6
7
8
9
10
11
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;
}

用简单的add指令作为示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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 esi,eax
;转换为虚拟机的指令如下:
vPushReg32 eax_index
vPushReg32 esi_index
Vadd
vPopReg32 esi_index ;不弹eax_index,它作为返回结果保存在堆栈里

image-20220322153743802

​ 上图是参考文章中的图,和我自己画的那个差不多,区分了颜色来说一下蓝-橙-黄部分作用(一条vmp指令的执行)

    * 蓝色:加载并解密字节码。
  • 橙色:根据字节码数值计算出handler地址。
  • 黄色:执行该handler逻辑。

​ 前十几个蓝橙黄组合都是一种handler,执行的是一种vPop操作,如上图箭头,是将真实寄存器的值移动到VMcontext区域,一个vPop移动一个值。

​ 后十几个蓝橙黄组合

image-20220322160413449

​ 最后一个vRet,将压入vmp运算栈中vmp虚拟寄存器值,更新到真实cpu寄存器中,至此,退出虚拟机。

image-20220322160612021