探秘VMProtect

探秘VMProtect

VMProtect之所以叫做VMProtect,因为它是以VM(Virtual Machine)虚拟机为核心来实现的,这里的虚拟机并不是传统意义上的虚拟机,其是将汇编指令进行虚拟化,让其失去原本容易理解的含义,增大对逆向工程的难度。

一.虚拟机

1) 虚拟机指令(VM_Handler)

VMP中的虚拟机指令可分为一下几类:

出入栈:PushReg、PushImm、PushEsp、PopReg、PopEsp…

运算:Add、Div、IDiv、Imul、Mul、Nor、Shl、Shr…

内存读写:ReadDs、ReadFs、WriteDs、WriteFs…

一些特殊的指令:GetHash、Jmp、Cpuid、CallApi、Rdtsc…

当然还有一些其他的如浮点运算的指令。

在这些指令中又可根据操作数大小的不同来进行细分。VMP将原程序的指令进行分解转换为由这些Handler组成的新的指令操作集合,实现程序的虚拟化。这些Handler的地址存放于一张指令表中,通过调度执行。从Opcode的解码过程中可以看出,该表的大小为0x100。

2) 虚拟机调度(VM_Dispatch)

VMP中的虚拟机调度一般以这样的形式存在:

3) 虚拟机Context

VM_Context主要涉及对真实环境中上下文的保存,以及一些数据的传递。其组成主要有:

其中的寄存器存储位置在每个虚拟机中都有所不同。VMP中对一些一些寄存器的使用也有规定比如:ESI记录opcode的位置(VM_Eip),EBP记录VM使用堆栈的位置(VM_Esp),EDI中储存着VM_Context的偏移。

4) 虚拟机入口执行过程

以下是一个真实虚拟机入口的执行流程:

5) 虚拟机调度过程

虚拟机从initkey数据解码得到Opcode地址,然后逐个提取解码Opcode,交由调度模块执行相应的VM指令。

图片1

二.虚拟机的识别

1) 获取指令的use-def链

首先需要读取指令流,并且记录每个指令对哪些数据进行了定义或者使用(引用)了哪些数据,组成简单的ud-chain。在VMP的混淆优化中,需要进行ud记录的数据主要是寄存器、堆栈位置数据以及ebp相关。在读取指令流的过程中,遇到可以进行跳转识别的指令也需要进行处理,比如遇到jb xxxxxxxx的跳转指令时,它的前一个指令是sub esp,-4,则可以从标志位的变化得出该跳转是必定实现的,读取指令就跳到地址处继续。

2) VMP中混淆的优化

VMP中混淆可分为几类:

乱序:乱序就是将代码段分散到内存中不同位置来执行,使用跳转指令来连接。对于乱序混淆,在识别跳转读取指令链的时候就可以很好的处理。

无效指令:VMP中的无效指令一般就是对某个数据的重复定义。如如下混淆:

这里第1、2个指令对eax进行了重新定义,而在这之间有没有对eax的引用,因而第1个指令就是无效指令。而2和4之间因为有对eax的使用,所以不能将2作为无效指令,除非第3个指令已经被定义为无效指令。

堆栈混淆:VMP中基于堆栈的混淆类似于:

堆栈上混淆的优化需要记录每个指令对esp的操作大小以及对堆栈位置的引用,在优化时通过恢复堆栈的大小来判断之前哪些是无效的入栈指令。

3) VM_Context识别

从虚拟机入口开始,记录堆栈位置以及每个堆栈位置中数据的类型(寄存器或其他),context数据的入栈顺序是initkey->retaddr->原环境寄存器->antidump->relocation。从头遍历指令链,遇到有对堆栈位置数据进行定义的,再根据指令来判断是什么类型的数据。

4) VM_Dispatch识别

遍历指令链找到mov Reg0,dword ptr ds:[Reg1*4+const]格式的指令,如果const的值在模块范围内,该指令一般就是调度指令,const就是VM_Handler表的首地址。

5) VM_Handler识别

在获取VM_Dispatch后,从VM_Dispatch开始遍历,获取所有对Reg0进行定义指令,组成的指令链为handler的解码指令合集。然后创建一个虚拟的指令执行体来执行解码指令,这里可以对个指令进行模拟,也可以将指令的二进制码注入到自己的程序中来执行。

通过VM_Handler表的首地址获取0-0xff范围内所有的指令地址码,然后通过执行解码指令获取真实的Handler地址。获取地址后,读取已该地址为首的指令链,最后通过匹配特征码来解析出真实的Handler。在VMP中虚拟机的堆栈位置(VM_Esp)是在ebp中记录的,所以对ebp指定位置有定义或者引用的指令都能拿来作为特征指令,另外特征指令也可以是esi(VM_EIP)相关、关键操作指令、特殊指令等。比如VM_ReadDs32的指令特征:

VM_CPUID的特征指令:

VM_Nor32的指令特征:

三.VMProtect Hash Check 分析

1. Hash Check

VMP使用VMProtectIsValidImageCRC来进行内存检测,一个关键的Handler就是VM_GetHash,以下是一个完整的VM_GetHash:

 

去除花指令和混淆后,VM_GetHash可以化简为:

 

 

GetHash入口VM堆栈:

 

Hash 元素的结构如下:

需要校验的Hash数据被加密存储在一张表中,VMP使用VM_ReadDs32从内存中读取hash元素数据,我这里写了个脚本来观察过程,在VM_ReadDs32和VM_Hash下断,获取相关信息。

 

可以看出VMP会先从表中读取两个数据,按偏移也就是Hash元素中的Address和Size,之后调用VM_GetHash获取给定地址和大小的Hash值,与再一次读取到的Hash元素中Value值进行比较。

在完整的结果中可以看出,调用VM_GetHash分为4个阶段:

第一阶段文件Hash校验,该阶段调用hash次数不是很多,但size一般较大,目的是校验文件的完整性。

第二阶段是Code 校验,就是在壳执行过程中对壳代码数据的校验,检测patch代码或者Int3断点。

该部分会先对Hash表的数据完整性进行检测,因此可以从第一个VM_GetHash中获取到该阶段Hash表的地址和大小。

第三阶段是内存校验,该阶段是在原程序数据解码后进行的,主要是对原程序数据的完整性校验,该阶段和第二阶段一样,第一个VM_GetHash会先对Hash表进行检测。

第四阶段是随机校验,其会先调用VM_Rdstc获取一个随机数,随机对内存中数据进行校验。

 

2. Patch Hash

这里Patch Hash有两种方案,第一种是将Hash表的数据进行替换,然后对VM_GetHash进行Patch,让它放回同个值。

第二种是对Hash检验中的循环变量进行修改,取消循环次数。在运行到VM_GetHash时VM_Context(edi)中的某个值就是循环变量,但这个变量的位置在每个虚拟机中是不同的,需要手动观察判断,下面是某个VM_GetHash中的

 

其中000CF6D0位置的值就是循环变量,在这里将值改为1就能跳过接下来的Hash校验。

对于第四种随机Hash校验,需要Patch VM_Rdstc使其返回某个固定的值,最后每次Hash校验固定的位置。

 

四.脱掉VMProtect

 

1. LocalAlloc Anti-Dump

为了防止程序被转储脱壳,VMP会从多个方面来AntiDump,其中第一个就是申请内存并在内存中放入一些关键数据,在VMP运行过程中会读取这些关键数据进行验证,如果脱壳后没有处理这部分数据,运行过程无法读取到相应的数据,程序将无法运行。如下是记录LocalAlloc Anti-Dump的过程:

其中省略了一些对数据加密的指令,只记录保留一些关键指令和写数据指令,从中可以看出,在LocalAlloc申请到内存后,VMP会向里面写入0x14*4=0x68大小的加密数据,之后会通过CPUID获取CPU的硬件绑定信息加密后存入anti-dump内存。

所以要处理anti-dump不仅要对加密数据进行保存,还需要Patch VM_CPUID使其返回相同的值,这样才能完整的跨平台处理LocalAlloc Anti-Dump。

2. Heap Anti-Dump

VMP在壳运行过程中会调用HeapCreate来创建堆内存,用来储存一些关键数据(尚未清楚),以及提供后续解码资源数据的内存空间,所以该堆内存会在程序运行过程中将被申请使用。VMP对这部分代码并没有VM,但对API进行了隐藏,下面是调用HeapCreate部分:

申请的堆会在程序运行时随时被调用,因此要处理Heap Anti-Dump需要dump heap 内存,并提供足够的空间(因为后续会多次申请,heap内存需要足够)。

3. Resource Anti-Dump

VMP对资源的处理个人觉得是难以完全修复的,能用的方法是沿用VMP对资源的处理。VMP在壳运行阶段会对4个资源相关的API进行HOOK,这4个API为:LdrFindResource_U、LdrAccessResource、LoadStringA、LoadStringW。具体的HOOK只是跳转到VMP自身模拟的API中,

 

并且VMP也会申请内存记录被hook的代码,使执行完自身的代码后又去调用真实API执行。在申请这部分内存时,VMP会从两个API相关模块的地址结束开始每次增加0x1000大小来调用VirtualAlloc申请内存,直到这两块内存被申请。

第一块申请hook内存:

第二块申请hook内存:

处理VMP资源的anti-dump,需要添加自己的代码去hook这4个api到相关地址,并且为实现跨平台需要自己实现hook的调回。

4. OEP定位

关于VMP OEP的查找网上也有很多的方法,其中最简单的就是在最后一次VirtualProtect后,对代码段下内存访问断点。先来看一下壳运行时VirtualProtect调用情况:

第一次调用:

可以看出首次调用VirtualProtect是先将代码段内存的权限变成可写,这是在为数据的解码做准备,从中也可以看出代码段的基址为01001000,大小为 7748。

第二次调用:

此次断下后,可以看出壳又将代码段权限改成了不可写,说明数据已经解码完毕,查看代码段数据也已经不为0。数据解码后,当然很快就要将程序控制权交给原程序代码了,OEP也即将到达,此时在代码段下访问断点,断下后就在OEP或者OEP附近了。在这里其实最好是在VM_Retn出口下断,VMP退出虚拟机后就要将eip转给原程序了,在OEP被VM的情况下,如果在VM_Retn出口处进行OEP的修补,工作量会小很多。

5. IAT(API)修复

VMP对API进行了混淆,它将所有IAT相关的API调用都转变成为pushx;call或者call;xxx的形式,这对修复造成了不小的影响,也对程序的理解造成了困难,下面是某个被混淆的API调用指令流程已经相关esp信息:

从中获取最主要的解密指令:

观察几个API的混淆可以发现,所有的API就是参照如下规则解密的:

所以要获取API地址只要获取const0、const1、const2这3个常数就行了。另外光知道API地址还不行,还需要知道它的调用类型。这里需要观察指令链最后的ESP位置,以及最后入栈的指令所引用的esp地址。

在上述指令链中:

最后入栈的指令所引用的esp地址为0,最后Esp pos为4。经过观察分析,调用类型一般可分为以下几类:

按照这样大部分的api可以完全修复,最后不能修复的几个是与资源相关的api,只要资源anti-dump修复正确,这几个api可以不去管。

6. 脱壳总结

VMProtect保护的脱壳修复的繁琐是免不了的,对于anti-dump可以采用补区段的方法,为了dump区段的位置紧凑,可以在系统断点时就在模块后面申请相关内存区段,然后再脱壳是patch一些anti-dump位置到该区段。

Api的修复可能不仅仅只对代码段,在壳区段中可是需要修复一些api的,这暂时还没找到合理的解决方法。

五.VMProtect 代码还原尝试

 

1. 代码静态识别

在VM代码识过程中,需要注意的是3个寄存器的使用:ESI作为VM_Eip也就是Opcode的地址,它在每次虚拟调度的过程中都会改变,在一些虚拟指令包含的数据也是Opcode的一部分;EAX是Opcode或者Opcode数据读取以及解密时使用的相关寄存器;EBX则是全程参与解密的寄存器。下面是某虚拟机入口的Initkey解密VM_Eip的过程:

虚拟机从InitKey解密得到VM_EIP,将没加重定位前的esi做我EBX的初始值。Opcode的解密流程如下:

数据的导入从Opcode读取进行解密,其中也有EBX的参与,下面是某VM_PushI32的数据导入解密过程:

对于之前版本的VM_PushR32与VM_PopR32寄存器偏移的解密不是从Opcode数据中获取的,而是直接从该Opcode解密得到:

 

静态解密引擎的主要实现就是对这些解密指令的提取以及模拟执行。提取这些解密指令的要点就是识别对ESI、EAX、EBX这3个寄存器的定义以及引用。然后再引擎中实现一个虚拟机循环,对Opcode和相应数据解密并静态输出,直到遇到VM_Jmp或者VM_Retn退出。VMP中解密指令固定涉及这几个寄存器并且不会涉及到堆栈的操作,因此在模拟执行的过程中可以直接注入这些指令到当前的程序来执行解密。

静态解密到VM_Jmp后将没法确切获取将跳转的VM_Eip地址,因此需要程序执行到VM_Jmp再重新进行下一步解密,这进一步的解密需要获取关键的ESI和EBX的确切值才能继续,要获取完整的虚拟机执行代码还是需要结合动态的执行。

 

2. 寄存器轮转

虚拟机入口在调度过程中,首先会将入口入栈的Context数据pop到相应的VM寄存器中,隐藏真实的context含义,很好的实现了混淆的目的。一下是某个虚拟机起初调度的过程:

–>

在VM_Retn中,又会将隐藏的真实数据赋值到真实的Context中:

–>

真实寄存器的虚拟机寄存器中的位置是没法预测的,即使知道虚拟机入口出口的context数据出入。在虚拟机中还会遇到寄存器的轮转,比如在遇到VM_Jmp时的情况:

经过寄存器轮转后寄存器所代表的含义进一步被打乱。

3. 分析尝试

以下是对一个简单代码(就加密了一句汇编)虚拟机加密的分析:

 

该虚拟机简单的过程描述:

1) Pop Context到相应VM寄存器

2) Push 正确跳转VM_Eip和检测到单步调试后的VM_Eip地址数据

3) 检测EFL中的TF标志位

4) 根据检测结果从堆栈读取跳转数据

5) 解密跳转数据

6) 寄存器轮转入栈

7) VM_Jmp

8) 寄存器轮转出栈

9) Push VM_Retn address 数据

10) 真实代码执行,这里是Xor eax,eax

11) 真实寄存器数据入栈

12) VM_Retn

 

My vmprotect od plugin:

https://github.com/OoWoodOne/VMP_ODPlugin

发表评论

电子邮件地址不会被公开。 必填项已用*标注