VMP加壳(二):VMP的虚拟化原理
介绍VMP虚拟化原理之前,先简单介绍一下计算机运行的原理。总所周知,现代计算机的核心部件是CPU、内存、磁盘、键盘、显示器等;最最最核心的就属CPU、内存和磁盘了。用户按开机键,CPU会把OS从磁盘加载到内存运行。由于CPU只能识别并执行二进制文件,所以代码、数据等都是以二进制存放在磁盘和内存的。
1、为了在软件层面“虚拟化”出底层的硬件,让OS顺利在虚拟机运行,虚拟机也要提供CPU、内存和磁盘的“虚拟”环境,让二进制代码得以顺利执行。目前主流的虚拟化技术是intel/amd推出的VT技术,让VMware、virtualBox这种虚拟机可以“直接”让硬件CPU执行二进制代码,极大提升了虚拟机的效率和速度,减少了模拟的性能损耗。但这种虚拟机的功能相对较重,并且直接让硬件CPU执行虚拟机的二进制代码,达不到保护、混淆代码的目的,所以这种硬件虚拟化是不适合做代码保护的,只能考虑通过纯软件模拟虚拟机执行代码指令。
为了在软件层面模拟CPU执行二进制的代码指令,需要有以下关键点:
- 虚拟机的寄存器,用来存放各种临时数据
- 虚拟机的堆栈,用来做各种数据交换
- 虚拟机的指令。x86架构下,CPU一旦读取到0x55指令,就知道执行push ebp;一旦读取到0x8BEC,就知道执行mov ebp,esp; 同理,虚拟机也需要有自己的指令集,虚拟CPU才知道自己要干啥。一般虚拟机的指令要么是操作寄存器,要么是操作堆栈,要么做各种算数运算,虚拟机指令的handler都要模拟这些功能。那么问题来了,虚拟机的指令集能不能和物理CPU一样了? 显然是不行的! 两个原因:(1)如果一样,还要纯软件虚拟机干啥? (2)如果一样,达不到混淆指令的目的
- 虚拟机的EIP,用来指明虚拟CPU当前执行的代码
为了满足以上关键点,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读取虚拟机的指令
(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)这几个步骤;
整个过程展示如下:
(5)综上所述,要想全面了解、分析和掌控VMP,必须要找准这么几个点:
- VM_DATA:虚拟机的指令都集中在这了
- VM_CONTEXT:虚拟寄存器都保存在这里
- diapatcher:所有指令都从这里路由到对应的handler执行(可以简单理解为管理层派发活的,不过3.x版本的VMP貌似去掉了统一的dispatcher,由上个handler直接跳转到下个handler,有点P2P、区块链去中心化的感觉)
- handler:具体模拟执行虚拟指令的分支(可以简单理解为具体干活的工具人)。handler之间跳转通过jmp esi 或 push esi ,ret等指令实现(不同版本使用的寄存器可能不同,但跳转实现的方式就这些);
- vEIP:当前执行的指令,需要明确是由那个物理寄存器保存的
- vStack:存放了临时数据用于各种交换,目前版本还是由
3、上面讲了大段各种理论,下面说说具体怎么找这些关键点(注意: 下面OD分析的截图不是一次调试截取的,事实上我测试时反复调试了十几次,每次用OD打开样本的地址都不同,但不影响代码执行的顺序和关键点的分析)。
(1)demo代码如下:这里模拟一个密码、lisence之类的场景:正确的密码是123,输入后输出ok;输入错误的密码就输出fail;
#include <stdio.h> #include <stdlib.h> #include <Windows.h> char buf[1204]; void main() { while (1) { scanf("%s", buf); if (!strcmp(buf, "123")) printf("ok\n"); else printf("fail\n"); } }
(2)这里只把main函数虚拟保护即可:
(3)虚拟化后的结果:从40K增加到602K,增加了15倍;
用OD打开:VMP0段564K,膨胀的部分都集中在这里了;
(4)刚进入OD,看到的全是jmp。根据之前的理论分析,这些jmp构成了handler表:
在刚才那个内存视图,选中vmp0段,右键选择内存访问断点,后续只要访问这个段就会立即断下来。然后开始运行,断到下面:push xxxx,call xxxx这是非常明显的VMP3.xx版本的壳特征:
不停F7单步进入后来到这里:看到了好多push 寄存器的指令,这里就是保存原始物理寄存器的值的,为后续进入VM加壳做准备;可以看到在push指令之间加载着大量没用的垃圾指令;
这些push指令执行完后,栈如右图所示;注意这里一行关键代码已经标红: esp+0x28处的值赋给了edi,edi指向了VM_DATA,也就是虚拟指令集(后面会进一步解密edi);
经过下面add edi,edx后,得到真正的VM_DATA地址:
继续往下:又有一条关键指令:sub esp,0xC0; 这里把esp往上开辟192byte的空间,作为VM_CONTEXT(也就是虚拟存器)。后续会用esp+edx来读取这些虚拟寄存器;此时堆栈图如右边:下面保存的是物理寄存器,上面是VM_CONTEXT,中间以ebp隔开;
继续F7(后续能看到大量这类似的代码):通过ebp从栈取原物理寄存器的值,然后通过edi取指令,接着根据指令对取出的数做各种运算:
计算完毕后写回VM_CONTEXT:
接着继续取指令和栈的值:
计算完毕后继续写回VM_CONTEXT:
期间还检查虚拟栈是否填满:
栈顶和栈底比较,看看谁大:
如果栈够用就通过jmp继续执行下个handler:
如此往复好多次,都快把VM_CONTEXT填满为止,终于到了while循环。这时不知道密码是多少,先随便输入,看到程序输出了fail,突破点就在这了: 通过ASCII在内存中找fail,然后下个访问的断点,成功断下来:
这里回溯栈:在栈中保存了函数的调用关系,这里肯定有while循环、判断对比的函数:这里把password在栈上所有的函数都轮询个遍(kernerl32、ucrtbase这些windows系统自带的API就没必要了),挨个下断点,一共下了十几个:
这里用辨识度比较高的字符串"aaaaaaaaaaaaa"输入,然后挨个断点地检查,终于在一个断点处找到了密码字符:这个大概率是strcmp函数
总结:1、大段代码被人为切割成一小块一小块地执行,代码之间通过jmp、call、push+ret方式跳转
2、统一的dispatcher是真的没有了,handler倒是有一个大表(整个程序的入口点也改到这里了),里面全是jmp语句,对应不同的handler分支
3、esp指向VM_CONTEXT,edx是虚拟寄存器的偏移,通过esp+edx定位虚拟寄存器的位置;读取的虚拟寄存器保存在ecx,各种处理后通过ecx写回虚拟寄存器;
ebp指向虚拟栈顶,通过ebp+偏移挨个读写虚拟栈的数据;
4、取指令、解密、执行、跳转到下一个handler:很多代码都是一样的,重复生成了好多次
参考:1、https://bbs.pediy.com/thread-225262.htm 新手篇VMProtect 1.81 Demo
2、https://mp.weixin.qq.com/s?__biz=MjM5NTc2MDYxMw==&mid=2458296943&idx=1&sn=8ba937d6216a37025d5f97d8a4989f4a VMProtect 3.3.1虚拟机&代码混淆机制入门
3、https://bbs.pediy.com/thread-225803.htm 如何分析虚拟机(2):进阶篇 VMProtect 2.13.8
4、https://bbs.pediy.com/thread-224732.htm 谈谈VMP的爆破