0.VMP的工作原理

1.VMP实现虚拟化的方案

  • 虚拟机的寄存器:在内存开辟一段连续的区域当成虚拟机的寄存器,业界称之为VM_CONTEXT,某些版本的VMP用EDI指向这个区域
  • 虚拟机的堆栈: 这个和物理机是一样的,直接在内存开辟就好。VMP还是用EBP指向栈顶
  • 虚拟机的指令:不同版本VMP的指令是不一样的,这样可以在一定程度上防止VMP本身被破解,业界俗称VM_DATA
  • 虚拟机的EIP:业界俗称veip,某些版本的VMP用ESI替代,指向VM_DATA,用以读取虚拟CPU需要执行的指令;

2、VMP虚拟机的执行流程

  (1)想想启动VT时,是不是要先开辟一段内存空间,把当前guestOS部分寄存器的值保存好?VMP也一样,先保存物理寄存器的值,后续退出VM后才能还原

​ (2)让vEIP从VM_DATA读取虚拟机的指令(pcode)

​ (3)由于虚拟机的指令和物理CPU完全不同,那么在指令读取后,该怎么去执行了?举个栗子:比如0x1表示入栈,0x2表示出栈,0x3表示寄存器之间互相传数据(当然实际的指令可能不会这么简单,VMP每个版本的指令集都不同),这些指令该怎么执行了?在VMP中,有个概念叫handler,专门根据不同的指令执行不同的操作(当然这些操作VMP事先都定义好了)。这个和VT中VMX的handler作用类似:根据不同的异常有不同的处理方法(我个人猜测VMP的作者肯定借鉴了VT的原理和思路);

   为了达到这种不同指令执行不同handler分支的效果,编码实现层面通常用switch+case实现,用于将不同的指令跳转到不同的分支执行,业界俗称dispatcher。具体到汇编代码,switch+case一般的汇编形式为:mov ecx,dword ptr ds:[eax*4+base](注意寄存器可能会变成其他的,但这 xxx*4+基址的形式不会变, 这是比较明显的特征,用以用来定位VMP的dispatcher。

  (4)执行完一个handler,vEIP接着指向下VM_DATA的下一个指令,然后重复(2)-(4)这几个步骤;

以VMP1.1版本的加密为例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
__declspec(naked) void test() {
__asm {
mov eax, 0x12345678
ret
}
}
int main()
{
test();
std::cout << "Hello VMP!\n";

system("pause");

return 0;

}

对test()函数内部代码进行VMP加密,x32dbg调试

1.mov eax 0x12345678指令进行了加密

0x40AFDD 就是pcode的地址

加密逻辑

下面讲一下VMP中虚拟指令Handler的分类,并介绍了一部分比较重要的指令

1、元指令

来看一个具体的例子,vPushImm4,这是1.7Demo版本没有混淆的Handler实现:

虚拟机栈顶Vesp就是ebp

再来看一个虚拟寄存器入栈的指令:vPushReg4

2、算术运算指令

接下来我们来看一下算术运算指令。最经典的当属加法指令vAdd4:

这样一来,这条vAdd4虚拟指令执行完毕之后,栈顶第一个值就是最新的eflags,第二个值就是加法的和了。

再来看一个逻辑右移指令vShr4:

这个Handler中,从栈顶读取4个字节的被操作数放到eax寄存器,从栈顶+4的位置读取1个字节的移位计数放到cl寄存器。然后抬高栈顶2个字节,执行移位操作,把移位的结果放到新的栈顶+4的位置。最后和加法一样,移位操作同样会涉及eflags寄存器的修改,也要把最新的eflags保存到栈顶。

这个图描绘了指令之前之前和执行完成的堆栈变化情况:

3、内存操作指令

接下来这一组是内存操作相关的虚拟指令,我们看一个例子vReadMemSs4:

第一步:从堆栈顶部读取4个字节装入eax

第二步:以刚刚读取到的eax的值作为指针,读取指向的内存区域,大小是4个字节,同样装入eax寄存器中。这里读取内存的时候,使用的段寄存器是ss,所以这个Handler命名里面有个Ss。

vReadMemSs4相当于mov [ebp],[[ebp]]

4、逻辑运算指令

他的操作流程就是

1
2
mov [ebp+4],and((not)[ebp],not([ebp+4]));
mov [ebp],新的eflags;

5、跳转指令

直接从栈顶的位置取了4个字节赋值给了esi寄存器,然后ebp+4,更新栈顶指针位置就完成了。