专栏名称: 吾爱破解论坛
吾爱破解论坛致力于软件安全与病毒分析的前沿,丰富的技术版块交相辉映,由无数热衷于软件加密解密及反病毒爱好者共同维护,留给世界一抹值得百年回眸的惊艳,沉淀百年来计算机应用之精华与优雅,任岁月流转,低调而奢华的技术交流与探索却
目录
相关文章推荐
花果科技  ·  iOS 微信新功能,可开启面容 ID 登录 ·  17 小时前  
学习强国  ·  下月起,这种发票将在全国推广 ·  3 天前  
似水之流年  ·  互联网的流量密码 ·  4 天前  
似水之流年  ·  互联网的流量密码 ·  4 天前  
勤于奋  ·  一个赚美金网站 ·  5 天前  
奶爸投研干货基地  ·  11/21 小心反弹结束 ·  1 周前  
奶爸投研干货基地  ·  11/21 小心反弹结束 ·  1 周前  
51好读  ›  专栏  ›  吾爱破解论坛

【原创】VMProtect 3.09 虚拟机架构浅析

吾爱破解论坛  · 公众号  · 互联网安全  · 2017-03-10 11:09

正文

-------

由于原文代码较长,部分代码使用截图代替,具体内容可以参看论坛原文。

------


0x00 写在前面

最近两周一直在研究VMProtect相关的东西,发现VMProtect 2.x 被研究的比较充分,FKVMP、Zeus、VMP分析插件1.4等神器也出来了很久。但VMProtect 3 没有在论坛内找到相关的资料,这里写一点简单分析作为抛砖引玉之用。新人第一贴,求论坛的大大们多多鼓励。


根据官方公告,VMProtect 3使用了新的虚拟机架构,本文通过分析自己加虚拟化的简单程序,介绍一下VMProtect 3虚拟机的基本架构,并与VMProtect 2.x进行简单对比。

这里强调一下,本贴只介绍虚拟机架构方面的内容,关于脱壳、IAT恢复、过anti等等请参考其他大佬的文章。


0x01 初步分析

分析的样本是用MASM32编写的小程序,源代码如下:


编译完成后大小1.50 KB (1,536 字节)


使用VMProtect 1.81 demo版本对main函数(地址0x40100C)加虚拟化,最快速度,关掉一切其他保护。

这里之所以使用 1.81 demo这么低的版本,主要原因是该版本VMP中的虚拟机代码没有混淆,生成的bytecode也比较规则,没有2.x版本的冗余分支,同时又能被VMP分析插件1.4完美支持,因此非常适合拿来学习VMP虚拟机结构。

加密后文件大小5.00 KB (5,120 字节)


再使用VMProtect 2.12.3版本加虚拟化,选项相同 ,关掉所有保护。

加密后文件大小15.0 KB (15,360 字节)


再使用论坛的VMProtect 3.09版本加虚拟化,选项相同,关掉所有保护。

加密后文件大小516 KB (528,896 字节)


可见VMProtect 3.x版本加密后的代码量有非常大的变化,注意这里关掉了所有保护选项的情况,因此这些多出来的代码不是壳代码,也不是调试器检测的代码,就是实实在在虚拟化后的指令。

这么为什么会多出这么多,这里简单分析一下。


需要用到OD的追踪功能(很好用的功能,但大家经常忽略掉他)

OD分别打开两个文件,Ctrl+F11 跟踪步入,64位电脑先将ntdll.dll和kernel32.dll标记为系统DLL,避免记录到trace中。

跟踪完成后分析两个程序产生trace的区别。


console.vmp_1.81_demo.exe  生成527条trace

用OD统计下指令数(注意在选项中关掉统计时将相邻指令合并的选项),前几条是这样



console.vmp_2.12.3.exe 生成14277条trace

用OD统计下指令数,前几条是这样



console.vmp_3.09.exe 生成2238条Trace



如果了解VMP架构的小伙伴应该发现问题了。我们具体比较一下

先说1.81,由于是demo版本,所以没有混淆,所以trace数相当的少。

2.12版本加了大量垃圾指令,同时也会额外生成几条bytecode,所以Trace数量猛增。

关键是3.09版本,其文件大小是2.12版本的几十倍,但trace数却只是2.12版本的几分之一。

再看指令数统计,答案就很明白了。


我们知道VMP 1.x 2.x的虚拟机结构都是典型的取指令、解码opcode、根据opcode进行dispatch的过程,整个虚拟机是个大循环结构,handler表最多0xFF大小,所有x86指令都由这0xFF(由于有重复,实际会少)个handler模拟完成。

执行次数最多的jmp dword ptr ds:[eax*4+0x403103]和mov ecx,dword ptr ds:[eax*4+0x405254]都是典型的dispatch指令。

由于循环的存在,很少的指令也会生成较多的Trace


反观3.x 可以看到只有5条指令重复执行了18次,剩下的指令最多只执行过2次。可以说3.x循环的部分是相关少的。那么这些取指令和dispatch的过程都是独立的。最为可怕的是handler本身也不是复用的。也就是说两条相同的指令会生成不同的handler代码,尽管他们实现的功能是完全相同的。因此文件大小会发生极大的变化。

(这里可能不确切,更大的可能是VMProtect的handler表变大了,由于测试指令过少,所以没有发生重复。如果真的每条指令都单独生成一系列handler,那加密指令多的时候文件会膨胀的可怕。但确定的一点是,原本复杂的循环结构消失了。)


0x02 版本对比


前面已经说过没有了循环结构,那么具体分析一下3.x的虚拟机是如何工作的。

首先简单说一下2.x的虚拟机,资料很多,不多说了,只提几个要点。


VMP虚拟机是栈式机,与x86体系结构的寄存器式虚拟机不同。x86的add eax, ebx指令,在VMP中会变成push ebx, push eax, add,  pop eax的形式。这里的栈称为虚拟栈。

EBP指向虚拟机栈顶,充当x86中的ESP功能,即虚拟机的vm_esp。

虚拟机执行的字节码是编码在程序中的,ESI指向其地址,ESI也就是vm_eip

EDI指向寄存器数组,也就是vm_context。2.x版本的vm_context也在栈中,在EBP上方。(其实最特别早期的VMP版本中vm_context也曾位于全局数据区,过段时间会写篇关于VMP各版本变化过程的文章,敬请期待)

vm_context包含16个4字节寄存器(32位的情况下)

栈结构是这样:

栈底--->ebp------>edi(vm_context)---> esp

EBX 经常参与解密,保存密钥。


说了关于旧版本的废话,这里直接抛出新版本的结构对比:

相同点:

ESI依然是vm_eip,指向要解析的字节码。

EBP依然是vm_esp,指向虚拟栈顶

EBX 依然经常参与解密。

不同点:

栈结构是这样:

栈底--->ebp------>esp(vm_context)

ESP成为vm_context指针,EDI作为跳转寄存器,统一的dispatcher消失,每次跳转需要从ESI取出4字节进行解密加到EDI上,然后jmp edi(或push edi,ret 功能一样)

这里ESP成为vm_context指针的设计对我们来说是个好消息,因为vm_context不能随便移动,因此涉及到栈操作的垃圾指令大大减少了。

但jmp edi的跳转方式很让人头疼。

FKVMP和VMP分析插件等神器的基本逻辑是找到dispatcher 即jmp dword ptr ds:[eax*4+0x403103]类似的代码

然后枚举eax的值0~0xFF即可得到所有handler的入口,根据指令特征识别handler。依次解密出bytecode。


3.x不再有统一的dispatch过程,使得之前的神器从架构上无法应用到3.x版本。


0x3 具体分析


前面是为了有个统一的印象。

接下来具体分析。


我们直接看前面OD得到的Trace,实际的trace中有大量的垃圾指令,但正如前面所说,多不涉及栈操作,因此比较简单,以下的分析均是去了垃圾指令的,所以会发现地址不太连续。


虚拟机初始化部分



跳转到第一个handler



这里就开始是handler部分了,与2.x 一样,最开始的几条bytecode指令是把栈中的真实寄存器pop到虚拟寄存器数组vm_context中



第一条handler结束后,又是跳转

虽然功能完全一样,但VMP又实现出了完全独立了另一套代码

以此规避掉了循环 



这里又是一个pop handler。



这里又是一个跳转。



这里又是一个pop handler



基本模式:

ESI保存bytecode和跳转地址,每条Bytecode前有4字节跳转地址

进行跳转时将跳转地址读出来,进行解密,解密后的值再加上EDI

就是最终的跳转地址,即handler地址。

按照惯例,虚拟机入口pop出所有寄存器到vm_context

应该会产生大量的pop指令,不一一分析了,这里直接跳过。


跳过的bytecode如下


vPopReg4 R1 (0x4)   

vPopReg4 R3 (0xC)

vPopReg4 R0 (0x0)

vPopReg4 R11 (0x2C)

vPopReg4 R12

vPopReg4 R2

vPopReg4 R13


执行完10个pop,这里终于出现了新handler, 是压入立即数指令,也就是mov eax, 0deadbeefh对应的bytecode。



又是跳转,但这次稍有不同,因为是push指令,需要检查栈是否发生溢出,这与2.x是一致的



这部分代码不在trace中,因为实际不会溢出,这里单独从OD中拿出来作为分析,同样都去掉了垃圾指令



接下来回到原本的分析过程中,由于重复了10遍mov eax, 0deadbeefh,

因此会出现10遍同样的handler,但地址各不相同


vPopReg4 R15(0x3C)

vPushImm4 DEADBEEF

vPopReg4 R4 (0x10)

vPushImm4 DEADBEEF

vPopReg4 R12(0x30)

vPushImm4 DEADBEEF

vPopReg4 R13(0x34)

vPushImm4 DEADBEEF

vPopReg4 R2 (0x08)

vPushImm4 DEADBEEF

vPopReg4 R9 (0x24)

vPushImm4 DEADBEEF

vPopReg4 R10(0x28)

vPushImm4 DEADBEEF

vPopReg4 R15(0x3C)

vPushImm4 DEADBEEF

vPopReg4 R4 (0x10)

vPushImm4 DEADBEEF

vPopReg4 R7 (0x1C)


这里可以看到同样的mov eax, 0deadbeefh ,pop的寄存器是不同的,可见2.x的寄存器轮转机制并没有变化。


新跳转



新的handler ,这次是push,用于退出虚拟机时保存虚拟寄存器到栈中



接下来的bytecode都是push指令,不一一分析了


vPushReg4 R11(0x2C)

vPushReg4 R0(0x0)

vPushReg4 R3(0xC)

vPushReg4 R1(0x4)

vPushReg4 R5(0x14)

vPushReg4 R6(0x18)

vPushReg4 R8(0x20)


又是一段跳转代码



新的handler 

vRet, 功能与之前一样,从栈中将寄存器弹出到真实寄存器中。



我们把1.81 demo的bytecode提取出来,这里用到VMP分析插件1.4这个神器(由于是demo版本,FKVMP是不行的)


00403765 VMP_0040100C  /$  78           vPopReg4 vR14        DWORD _t0 = 0

00403766               |.  6B F70C72B7  vPushImm4 0B7720CF7  DWORD _t1 = 0B7720CF7

0040376B               |.  39           vAdd4                DWORD _t2 = 0B7720CF7; DWORD _t3 = AddFlag(_t1, check00000000)

0040376C               |.  74           vPopReg4 vR13        DWORD _t4 = _t3

0040376D               |.  5C           vPopReg4 vR7         DWORD _t5 = 0B7720CF7

0040376E               |.  40           vPopReg4 vR0         DWORD _t6 = EAX

0040376F               |.  48           vPopReg4 vR2         DWORD _t7 = EDX

00403770               |.  64           vPopReg4 vR9         DWORD _t8 = EBP

00403771               |.  50           vPopReg4 vR4         DWORD _t9 = EDX

00403772               |.  74           vPopReg4 vR13        DWORD _t10 = ECX

00403773               |.  44           vPopReg4 vR1         DWORD _t11 = EDI

00403774               |.  54           vPopReg4 vR5         DWORD _t12 = ESI

00403775               |.  70           vPopReg4 vR12        DWORD _t13 = EFL

00403776               |.  68           vPopReg4 vR10        DWORD _t14 = EBX

00403777               |.  4C           vPopReg4 vR3         DWORD _t15 = 401016

00403778               |.  58           vPopReg4 vR6         DWORD _t16 = 403765

00403779               |.  FA EFBEADDE  vPushImm4 0DEADBEEF  DWORD _t17 = 0DEADBEEF

0040377E               |.  4C           vPopReg4 vR3         DWORD _t18 = 0DEADBEEF

0040377F               |.  E9 EFBEADDE  vPushImm4 0DEADBEEF  DWORD _t19 = 0DEADBEEF

00403784               |.  58           vPopReg4 vR6         DWORD _t20 = 0DEADBEEF

00403785               |.  23 EFBEADDE  vPushImm4 0DEADBEEF  DWORD _t21 = 0DEADBEEF

0040378A               |.  40           vPopReg4 vR0         DWORD _t22 = 0DEADBEEF

0040378B               |.  B4 EFBEADDE  vPushImm4 0DEADBEEF  DWORD _t23 = 0DEADBEEF

00403790               |.  60           vPopReg4 vR8         DWORD _t24 = 0DEADBEEF

00403791               |.  23 EFBEADDE  vPushImm4 0DEADBEEF  DWORD _t25 = 0DEADBEEF

00403796               |.  4C           vPopReg4 vR3         DWORD _t26 = 0DEADBEEF

00403797               |.  6B EFBEADDE  vPushImm4 0DEADBEEF  DWORD _t27 = 0DEADBEEF

0040379C               |.  7C           vPopReg4 vR15        DWORD _t28 = 0DEADBEEF

0040379D               |.  B4 EFBEADDE  vPushImm4 0DEADBEEF  DWORD _t29 = 0DEADBEEF

004037A2               |.  4C           vPopReg4 vR3         DWORD _t30 = 0DEADBEEF

004037A3               |.  6B EFBEADDE  vPushImm4 0DEADBEEF  DWORD _t31 = 0DEADBEEF

004037A8               |.  6C           vPopReg4 vR11        DWORD _t32 = 0DEADBEEF

004037A9               |.  E9 EFBEADDE  vPushImm4 0DEADBEEF  DWORD _t33 = 0DEADBEEF

004037AE               |.  40           vPopReg4 vR0         DWORD _t34 = 0DEADBEEF

004037AF               |.  23 EFBEADDE  vPushImm4 0DEADBEEF  DWORD _t35 = 0DEADBEEF

004037B4               |.  58           vPopReg4 vR6         DWORD _t36 = 0DEADBEEF

004037B5               |.  28           vPushReg4 vR10       EBX DWORD v0 = EBX

004037B6               |.  30           vPushReg4 vR12       EFL DWORD v1 = EFL

004037B7               |.  14           vPushReg4 vR5        ESI DWORD v2 = ESI

004037B8               |.  04           vPushReg4 vR1        EDI DWORD v3 = EDI

004037B9               |.  34           vPushReg4 vR13       ECX DWORD v4 = ECX

004037BA               |.  10           vPushReg4 vR4        DWORD _t42 = _t9

004037BB               |.  24           vPushReg4 vR9        EBP DWORD v5 = EBP

004037BC               |.  08           vPushReg4 vR2        EDX DWORD v6 = EDX

004037BD               |.  18           vPushReg4 vR6        EAX DWORD v7 = 0DEADBEEF

004037BE               |.  30           vPushReg4 vR12       DWORD _t46 = _t13

004037BF               |.  1C           vPushReg4 vR7        DWORD _t47 = 0B7720CF7

004037C0               \.  A4           vRet                 return Stack(34, 4)


对比可以发现其实bytecode是基本一致的,但是内部实现已经发生了巨大变化。


这里简单分析就结束了。

至于条件跳转等其他实现细节,以后再慢慢讨论


0x04 写在最后


从今年2月分开始的连续几个月时间可能都要研究虚拟机保护相关的内容,因此也会陆续发些心得笔记上来。

这里希望论坛大大们可以给些指点,好少走点弯路。

也希望其他坛友们能多多回帖讨论。


相关文件都放在附件中了(点击左下角原文按钮登录论坛下载)

解压密码www.52pojie.cn

深夜发贴,求鼓励


--官方论坛

www.52pojie.cn

--推荐给朋友

公众微信号:吾爱破解论坛

或搜微信号:pojie_52

推荐文章
花果科技  ·  iOS 微信新功能,可开启面容 ID 登录
17 小时前
学习强国  ·  下月起,这种发票将在全国推广
3 天前
似水之流年  ·  互联网的流量密码
4 天前
似水之流年  ·  互联网的流量密码
4 天前
勤于奋  ·  一个赚美金网站
5 天前
奶爸投研干货基地  ·  11/21 小心反弹结束
1 周前
奶爸投研干货基地  ·  11/21 小心反弹结束
1 周前
二更食堂  ·  2017最佳男友标准, 中10条才达标
7 年前
财税实务课堂  ·  这件事3月31日前必须去地税局办完不可!
7 年前