探秘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调用情况: