VMP study note. Mainly from "Encryption and Decryption". It record vmp analysis and how to recover vmp program process.

VMProtect逆向与还原浅析

 VMProtect是一个比较麻烦的壳,《加密与解密》第21章主要讲了这个壳,做一下笔记。此笔记分析的VMP版本为1.7vmp可以看作一个虚拟机+虚假跳转/流程分割的壳。下面是一个虚假跳转的例子,实际上下面的两个块都只执行了Mov dword ptr ss:[ebp+04h], eax语句。

image-20230706135706834

 因此,我们可以只选取一个路径,从而消除虚假跳转,最终得到如下流程图:

image-20230706142519890

  • VStartVM:负责从真实环境到虚拟环境的转换,开辟新的堆栈空间给虚拟机使用。具体来说,将除esp外的所有寄存器压入堆栈(最终将原始寄存器的值放入到VMContext中),而esp原来的值存放到ebp上。
  • VMDispatcher:调度分流。VHandler:不同功能的虚拟指令。
  • vCheckESP:检查堆栈,如果espVMP使用的堆栈)与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
2
3
4
5
vPushReg4 VR0   ; eax
vPushReg4 VR0 ; eax
vAdd4
vPopReg4 VR1 ; 标志寄存器
vPopReg4 VR2 ; 新的eax

 因此,如何将物理寄存器映射到VMP中的单赋值变量是一个难点。

 其次,由于动态调试无法完全遍历所有的分支路径,而静态分析的工作量又很大。因此,采用虚拟执行方法,建立一个类似于VMware的虚拟化系统环境,并在当前进程环境中加载可执行文件。利用虚拟执行的方法,我们可以在任何虚拟执行的地址处通过虚拟断点切换回真实的环境,读取虚拟机执行中产生的数据,跟踪所有可能会执行分支的虚拟指令。通俗的说,虚拟执行是运行VMP虚拟机的虚拟机。

利用虚拟执行生成完整的流程图

vjmp分析

vjmp的实现如下:

1
2
mov esi, dword ptr ss:[ebp+00h]
add ebp, 4

 这相当于直接修改了EIP指针。JCC的分析相对来说更加复杂,具体可以看《加密与解密第二版》P769。

虚拟执行不同的分支

 我们利用虚拟执行来使程序同时执行不同的分支。虚拟执行可以在执行指令时严格控制权限,还可以随时恢复虚拟堆栈与寄存器。其利用到了纤程(Fiber)的概念,它是比线程更小的执行单位,在一个线程中,每一个纤程都有独立的CPU寄存器结构与堆栈空间,可以随时从一个纤程切换到另一个纤程。书中自己实现纤程CPU环境的定义,并实现了切换纤程与退出纤程的函数。

 在vjmp指令之前,使用纤程来备份环境,并执行不同的路径。最终,可以执行完所有的路径,并生成完整的流程图。

识别Handler中基本块的虚拟指令

 可以这样理解,每一个Handler都有好多虚拟指令。可以使用模板匹配的思想,在vmpdemo版本(可以理解为简单版本)中提取原始指令作为模板,对正式版本vmp的每一个Handler进行匹配。如果一个Handler中的某个基本块可以匹配一个模板中的所有指令,就认为匹配成功。举例,下面是一个针对vPushImm4指令的Lua脚本模板:

1
2
3
4
5
6
7
8
9
10
11
12
13
do
local opcode = 'vPushImm4'
local asm = [[
add esi, 1|sub esi, 1|add esi,2|sub esi,2|add esi,4|sub esi,4
sub ebp, 4
<arg1> = eax
mov dword ptr [ebp], eax
]]
local action = function()
push(4, imm_t, 'arg1')
end
add(opcode, asm, action, Op_Assgin)
end
  • opcode表示模板对应的虚拟指令的名称。
  • asm定义了模板文本。其中,<arg1>=eax表示在其后面设一个虚拟断点,虚拟执行引擎在编译这个基本块时会在与之匹配的指令后面添加一个断点,当执行到这个断点时,取当时的eax的值并放到arg1中。

  • action定义了一个不会立即执行的动态函数。

  • add表示将前面的变量添加到匹配集合中。在匹配过程中,针对每个基本块执行一次对所有模板的匹配(针对基本块进行匹配)。

字节码优化

 由于VMP中的虚拟指令是基于堆栈的,堆栈式依赖于系统环境的,所以指令缺少变量之间的关联,我们可以将这些指令转换为不依赖堆栈而依赖于变量的多元表达式。例如,有这样一段字节码:

1
2
3
4
5
6
7
8
9
vPushReg  VR1
vPushImm2 2
vPushImm4 117CF08
vShr4
vPopReg4 VR2
vAdd4
vPopReg4 VR2
vPopReg4 VR3
use VR2, VR3

 分析后得到,其实现的功能为:

1
2
DWORD tmp1, VR2(elf0) = Shr4(117CF08, 2)
DWORD VR3, VR2(elf1) = vAdd4(VR1, tmp1)

 为了自动化实现这样的转换,我们可以将原始字节码变为:

1
2
3
4
5
6
7
8
9
var0 = vPushReg  VR1
var1 = vPushImm2 2
var2 = vPushImm4 117CF08
var3, efl0 = vShr4 var2, var1
VR2 = vPopReg4 efl0
var4, efl1 = vAdd4 var3, var0
VR2 = vPopReg4 efl1
VR3 = vPopReg4 var4
use VR2, VR3

 进行完上述步骤之后,我们可以清除某些无用的字节码,例如我们可以统计每一个变量使用的次数,对某些从未使用过的变量进行清除,并替换常量。统计结果如下:

1
2
3
4
5
6
7
8
9
var0(1) = vPushReg  VR1
var1(1) = vPushImm2 2
var2(1) = vPushImm4 117CF08
var3(1), efl0(0) = vShr4 var2, var1
VR2(0) = vPopReg4 efl0
var4(1), efl1(1) = vAdd4 var3, var0
VR2(1) = vPopReg4 efl1
VR3(1) = vPopReg4 var4
use VR2, VR3

 清除结果如下:

1
2
3
4
5
6
var0(1) = vPushReg  VR1
var3(1) = vShr4 117CF08, 2
var4(1), efl1(1) = vAdd4 VR1, var3
VR2(1) = vPopReg4 efl1
VR3(1) = vPopReg4 var4
use VR2, VR3

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
2
3
4
5
6
vPushReg4 VR1
vCpuid
vPopReg4 VR2
vPopReg4 VR3
vPopReg4 VR4
vPopReg4 VR5

 我们可以清楚的知道,VR1=eax;VR2=edx;VR3=ecx;VR4=ebx;VR5=eax

交汇点确定寄存器

vmp使用vPushReg/vPopReg来转静态单赋值寄存器中的值,但是此指令有二义性,既可以表示值传递,也可以表示映射传递(例如将VR1所映射的物理寄存器及其包含的值全部转移到VR2中)。文中先使用映射传递,后面寄存器出现冲突后再后退并分析,将其改为值传递。(但是此方法不一定有效,且效率低下)

 举例说明,例如有指令流程如下(假设都是传递映射):

image-20230706153932915

 那么,BB4就是交汇点。在这里补充一点,vmp通过寄存器分配算法在每个基本块中为每个虚拟寄存器映射了物理寄存器,而每个基本块的入口和出口都有一个入口寄存器映射表和一个出口寄存器映射表。由于前驱基本块的出口映射表与后继基本块的入口映射表不一定相同,因此要在每个基本块的入口处使用vPopReg指令,在每个基本块的出口处使用vPushReg指令。

 最终,可以得到:BB2.VR4=BB3.VR7=BB4.VR7=BB3.VR12

图着色算法确定其他寄存器

 上述4种方法只能确定一部分寄存器,而无法确定所有的寄存器。在此补充D-U链(定义使用链),为寄存器生成一个DU链代表:寄存器定义时所在的指令与最后以此使用时所在的指令之间的区间。通过k临近着色的思想,可以分配物理寄存器。具体细节看P787。

使用DAG匹配生成物理指令

 章节最后使用了DAG匹配生成物理汇编指令。正常来说,生成指令时通常采用树模式匹配。(树模式匹配将虚拟指令转化成抽象语法树的形式,然后使用指定的树模板进行匹配,当匹配成功时,将匹配到的树节点替换成一个新节点,并生成不同的汇编指令。)这里使用的时有向无环图(DAG)匹配,这是因为vmp的虚拟指令左边有多个表达式,例如有计算结果与标志位结果,如果抽象成语法树,那么将会有两个不同的根。如果将两棵树合并起来匹配,会非常麻烦。

 然而,使用DAG后,我们就要从虚拟指令的大的DAG图中匹配子图,对子图的匹配,其实是一个子图同构的问题,相应的效率最高的算法是VF2s算法,其在boost.graph中已经实现。

留言

2023-07-03

© 2024 wd-z711

⬆︎TOP