vmp-analysis
VMProtect逆向与还原浅析
VMProtect是一个比较麻烦的壳,《加密与解密》第21章主要讲了这个壳,做一下笔记。此笔记分析的VMP
版本为1.7
。vmp
可以看作一个虚拟机+虚假跳转/流程分割的壳。下面是一个虚假跳转的例子,实际上下面的两个块都只执行了Mov dword ptr ss:[ebp+04h], eax
语句。
因此,我们可以只选取一个路径,从而消除虚假跳转,最终得到如下流程图:
VStartVM
:负责从真实环境到虚拟环境的转换,开辟新的堆栈空间给虚拟机使用。具体来说,将除esp
外的所有寄存器压入堆栈(最终将原始寄存器的值放入到VMContext
中),而esp
原来的值存放到ebp
上。VMDispatcher
:调度分流。VHandler
:不同功能的虚拟指令。vCheckESP
:检查堆栈,如果esp
(VMP
使用的堆栈)与ebp
(真实堆栈)将要重合,则开辟新的堆栈地址(sub esp, xxx
),否则跳回VMDispatcher
。VRet
:此指令存在VHandler
中,目的是退出虚拟环境(将之前压入堆栈的寄存器值还原到物理寄存器中)。
在实际程序中,所有被VMP
虚拟化的函数的开头都会变成jmp VMEntry
的格式,我们以此为开头进行分析,就可以得到上图。上图中,有VMDispatcher
来调度指令,并根据指令索引跳转到相应的Handler
。相应的,有一个DispatchTable
调度表,其包含了VMP
中所有Handler
的地址,注册版本VMP
中的Handler
地址是加密的,而Handler
最多有256个。我们可以构建解密脚本,将256个Handler
的地址解密出来。将解密后的地址去重后,可以得到大概40个Handler
地址。
在VMP
环境中,CPU
寄存器的作用如下:
ESI
:当前要执行的字节码位置,相当于VM.EIP
。EDI
:指向VMContext
。EBP
:指向真实的ESP
。
0x00 VMP中的指令
VMP
是一个基于堆栈的虚拟机,也就是说其大部分的指令都是基于堆栈的,这样会使得其虚拟机指令的设计可以及其精简,且其没有cpu
寄存器的概念。但是还有一些指令是不基于堆栈的。还有一些指令与系统相关,VMP
无法模拟,例如cpuid
(获得cpu
相关信息)等,VMP
会先还原这些指令使用的寄存器,然后直接执行指令,最后将结果压入堆栈,重新保存在VMContext
中。
0x01 VMP的还原
由于VMP
没有寄存器的概念,因此VMP
使用了一种静态单赋值的形式,例如add eax, eax
可以被转换为:
1 | vPushReg4 VR0 ; eax |
因此,如何将物理寄存器映射到VMP
中的单赋值变量是一个难点。
其次,由于动态调试无法完全遍历所有的分支路径,而静态分析的工作量又很大。因此,采用虚拟执行方法,建立一个类似于VMware
的虚拟化系统环境,并在当前进程环境中加载可执行文件。利用虚拟执行的方法,我们可以在任何虚拟执行的地址处通过虚拟断点切换回真实的环境,读取虚拟机执行中产生的数据,跟踪所有可能会执行分支的虚拟指令。通俗的说,虚拟执行是运行VMP
虚拟机的虚拟机。
利用虚拟执行生成完整的流程图
vjmp分析
vjmp
的实现如下:
1 | mov esi, dword ptr ss:[ebp+00h] |
这相当于直接修改了EIP
指针。JCC
的分析相对来说更加复杂,具体可以看《加密与解密第二版》P769。
虚拟执行不同的分支
我们利用虚拟执行来使程序同时执行不同的分支。虚拟执行可以在执行指令时严格控制权限,还可以随时恢复虚拟堆栈与寄存器。其利用到了纤程(Fiber
)的概念,它是比线程更小的执行单位,在一个线程中,每一个纤程都有独立的CPU
寄存器结构与堆栈空间,可以随时从一个纤程切换到另一个纤程。书中自己实现纤程CPU
环境的定义,并实现了切换纤程与退出纤程的函数。
在vjmp
指令之前,使用纤程来备份环境,并执行不同的路径。最终,可以执行完所有的路径,并生成完整的流程图。
识别Handler中基本块的虚拟指令
可以这样理解,每一个Handler
都有好多虚拟指令。可以使用模板匹配的思想,在vmp
的demo
版本(可以理解为简单版本)中提取原始指令作为模板,对正式版本vmp
的每一个Handler
进行匹配。如果一个Handler
中的某个基本块可以匹配一个模板中的所有指令,就认为匹配成功。举例,下面是一个针对vPushImm4
指令的Lua
脚本模板:
1 | do |
opcode
表示模板对应的虚拟指令的名称。asm
定义了模板文本。其中,<arg1>=eax
表示在其后面设一个虚拟断点,虚拟执行引擎在编译这个基本块时会在与之匹配的指令后面添加一个断点,当执行到这个断点时,取当时的eax
的值并放到arg1
中。action
定义了一个不会立即执行的动态函数。add
表示将前面的变量添加到匹配集合中。在匹配过程中,针对每个基本块执行一次对所有模板的匹配(针对基本块进行匹配)。
字节码优化
由于VMP
中的虚拟指令是基于堆栈的,堆栈式依赖于系统环境的,所以指令缺少变量之间的关联,我们可以将这些指令转换为不依赖堆栈而依赖于变量的多元表达式。例如,有这样一段字节码:
1 | vPushReg VR1 |
分析后得到,其实现的功能为:
1 | DWORD tmp1, VR2(elf0) = Shr4(117CF08, 2) |
为了自动化实现这样的转换,我们可以将原始字节码变为:
1 | var0 = vPushReg VR1 |
进行完上述步骤之后,我们可以清除某些无用的字节码,例如我们可以统计每一个变量使用的次数,对某些从未使用过的变量进行清除,并替换常量。统计结果如下:
1 | var0(1) = vPushReg VR1 |
清除结果如下:
1 | var0(1) = vPushReg VR1 |
VMP
中在逻辑指令上,其实只实现了vNor
指令,其余指令如and|or|not|xor
都可以利用vNor
来实现。利用真值表,可以对逻辑指令操作进行化简。
寄存器映射(vmp的静态单赋值<->物理寄存器)
要进行vmp
的还原,必须要将其还原到物理寄存器。我们可以从4个位置确定静态单赋值与物理寄存器之间的映射。
VStartVM处确定寄存器
在VStartVM
处,会将物理寄存器压入到堆栈中,执行完毕后,转入到VMDispatcher
,其会将这些压入到堆栈中的寄存器弹出到静态单赋值VRxx
中,此时就可以建立关联。
vRet处确定寄存器
同理,在退出vmp
环境时,也会将静态单赋值VRxx
压入到堆栈,并弹出到物理寄存器中。
某些指令特征要求某些特定的寄存器
例如,cpuid
指令使用了eax
当作输入,以eax|ebx|ecx|edx
作为输出。vmp
在调用此指令时,其虚拟指令为:
1 | vPushReg4 VR1 |
我们可以清楚的知道,VR1=eax;VR2=edx;VR3=ecx;VR4=ebx;VR5=eax
。
交汇点确定寄存器
vmp
使用vPushReg/vPopReg
来转静态单赋值寄存器中的值,但是此指令有二义性,既可以表示值传递,也可以表示映射传递(例如将VR1
所映射的物理寄存器及其包含的值全部转移到VR2
中)。文中先使用映射传递,后面寄存器出现冲突后再后退并分析,将其改为值传递。(但是此方法不一定有效,且效率低下)
举例说明,例如有指令流程如下(假设都是传递映射):
那么,BB4
就是交汇点。在这里补充一点,vmp
通过寄存器分配算法在每个基本块中为每个虚拟寄存器映射了物理寄存器,而每个基本块的入口和出口都有一个入口寄存器映射表和一个出口寄存器映射表。由于前驱基本块的出口映射表与后继基本块的入口映射表不一定相同,因此要在每个基本块的入口处使用vPopReg
指令,在每个基本块的出口处使用vPushReg
指令。
最终,可以得到:BB2.VR4=BB3.VR7=BB4.VR7=BB3.VR12
。
图着色算法确定其他寄存器
上述4种方法只能确定一部分寄存器,而无法确定所有的寄存器。在此补充D-U
链(定义使用链),为寄存器生成一个DU
链代表:寄存器定义时所在的指令与最后以此使用时所在的指令之间的区间。通过k
临近着色的思想,可以分配物理寄存器。具体细节看P787。
使用DAG匹配生成物理指令
章节最后使用了DAG匹配生成物理汇编指令。正常来说,生成指令时通常采用树模式匹配。(树模式匹配将虚拟指令转化成抽象语法树的形式,然后使用指定的树模板进行匹配,当匹配成功时,将匹配到的树节点替换成一个新节点,并生成不同的汇编指令。)这里使用的时有向无环图(DAG)匹配,这是因为vmp
的虚拟指令左边有多个表达式,例如有计算结果与标志位结果,如果抽象成语法树,那么将会有两个不同的根。如果将两棵树合并起来匹配,会非常麻烦。
然而,使用DAG
后,我们就要从虚拟指令的大的DAG
图中匹配子图,对子图的匹配,其实是一个子图同构的问题,相应的效率最高的算法是VF2
s算法,其在boost.graph
中已经实现。
留言
- 文章链接: https://wd-2711.tech/
- 版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-ND 4.0 许可协议。转载请注明出处!