1. 原理
1.1 VMProtect Software 公司
VMProtect 软件公司成立于2000年,总部位于俄罗斯叶卡捷琳堡。该公司出品的软件保护软件 VMProtect(目前版本已更新到 3.x,以下简称VMP)可以说是软件破解领域的圣杯,多年来无数逆向分析人员前赴后继,一直试图揭开 VMP 的神秘面纱。
VMP 的特色功能包括虚拟化、混淆、反调试等。本文的测试内容主要针对的是VMP的虚拟化机制,即VMP 可以将受保护的代码放到内置的虚拟机中运行,以防止反编译和破解。
1.2 VMProtect 虚拟化原理
VMP 代码虚拟机是一款栈机,也就是说,VMP 的代码虚拟机与被保护程序的代码处于栈空间中。基于栈的虚拟机指令集主要是通过操作栈顶元素来完成。例如,Java 虚拟机(JVM)就是一个典型的基于栈的虚拟机。与之相对应的还有基于寄存器的虚拟机。指令操作数存储在虚拟寄存器中,指令集直接对寄存器进行操作。Lua 的虚拟机(称为 Lua 虚拟机,LVM)就是基于寄存器的。
而基于栈的 VMP 虚拟机因为要维护出栈入栈,所以执行同样的操作,需要的指令较多,间接使得执行效率较差。 VMP 会让程序变得臃肿、运行速度变慢,但被保护的程序代码膨胀后,也能给逆向分析人员造成很多困扰。
| 基本块名称 | 功能 |
|---|---|
| VStartVM | 从真实环境到虚拟环境的转换,开启新的虚拟空间给虚拟机使用,将除 esp 外的所有寄存器压入堆栈,esp 寄存器原来的值存放在 ebp 寄存器中 |
| VMDispatcher | 虚拟指令调度分流 |
| VHandler1..N | 不同功能的虚拟指令 |
| VCheckESP | 检查堆栈基本块,如果 esp (VMP 使用的堆栈)与 ebp (真实堆栈)即将被覆盖,则跟踪转到下一个基本块来开辟堆栈地址(sub esp, xxx),并将原来的 VMContext 中的内容复制到新的堆栈地址处,否则跳回 VMDispatcher |
| VRet | 这个指令存在于 VHandler 集合中。这个指令将堆栈中压入的寄存器值还原到物理寄存器中,然后退出整个虚拟机环境,并切换回真实的 CPU 环境。注意:这个指令的作用是退出虚拟环境,并不等于汇编语义上的 retn。 |
(表格出自《加密与解密(第4版)》)
VMP 虚拟化流程:
- VMP 将被保护的代码转换为虚拟机指令集(bytecode),并为每个虚拟指令对应一个处理程序(handler),每个 handler 实际上是一个小型的解释器。这些处理程序不是简单的地址,而是具体的处理逻辑代码。在代码转换过程中, VMP 还会对指令进行混淆和加密,以增加逆向工程的难度。
- VMP 还会对一些系统对象如文件、注册表等进行虚拟化,在虚拟机内部维护虚拟的系统资源,使得在虚拟机内部的系统交互不会影响到真实系统。 VMP 通常会结合多种反调试技术,例如代码自修改、反跟踪、反模拟执行等,增加调试难度。
- 受保护程序运行时,会首先执行插入的 VStartVM 代码。 (VStartVM 通常是作为程序入口点被插入到受保护代码中的 )VStartVM 负责将程序从真实环境转换到虚拟环境,并初始化虚拟机的上下文环境,将真实环境中的寄存器状态保存,并准备虚拟机所需的堆栈空间。此外,VStartVM 还会对一些关键的系统API进行hook,以截获系统调用并在虚拟环境中模拟执行。 VMP 允许在受保护代码块中插入特定的指令,用于主动切换回真实环境,例如调用未被 hook 的系统 API。
- VMDispatcher 负责从虚拟指令流中读取虚拟指令(bytecode),然后通过解码确定对应的处理程序(handler)。解码后的虚拟指令并不是直接执行真实指令,而是调用相应的 handler 来模拟执行,这些 handler 处理的是虚拟机指令,而不是直接的原始指令。
- VMP 使用两个堆栈:虚拟堆栈和真实堆栈。虚拟堆栈供虚拟机内部使用,而真实堆栈是线程原有的堆栈。为了避免两者冲突,VMP 会监控堆栈指针 esp。当虚拟指令需要修改真实寄存器时,真实寄存器的值会被临时保存在真实堆栈中,并将虚拟寄存器的值应用到真实寄存器上。如果 esp 与 ebp 即将发生冲突,VMP 会将当前虚拟机上下文保存到真实堆栈,然后开辟新的虚拟堆栈空间,确保虚拟机正常运行。对于多线程程序, VMP 会为每个线程维护独立的虚拟机上下文(VMContext),不同线程的虚拟机之间相互隔离。
- 虚拟化指令执行完之后,虚拟机并不会立即退出虚拟环境。只有在整个受保护代码块执行完毕或者发生异常时,才会退出虚拟环境,恢复真实环境中的状态。退出虚拟环境时,虚拟机会恢复原始的寄存器状态,并确保所有的上下文环境都正确恢复。不过在退出之前,还需要将虚拟寄存器的值写回到真实寄存器,并解除之前所做的系统API hook。
2. 测试程序
2.1 测试环境
系统环境:Windows 7 专业版(32 位)
IDE:Dev-C++ 5.7.1
加壳程序:VMProtect 3.4
2.2 测试程序源代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | #include<bits/stdc++.h>#include<windows.h>using namespace std;int main(){ printf("Please enter your pwd >> "); string s; cin>>s; if(s=="123456") printf("successful!\n"); else printf("failed!\n"); system("pause"); return 0;} |
编译得到CrackMe.exe
2.3 测试目标
在输入错误密码的情况下,程序仍可打印“successful!”
(测试演示出自B站up主Rairn,文末有提供视频链接)
3. VMP 编译测试程序
用 OD 加载CrackMe.exe,找到一个关键且易识别的位置,用 VMP 加密;在 OD 反汇编窗口点击右键,选择中文搜索引擎->智能搜索,搜索源代码中的字符串,例如successful
点进字符串successful对应的地址 0x00401702,分析一下反汇编代码,将加壳位置定为 0x004016B0,这看上去应该是一个函数的起始位置。将004016B0 55 PUSH EBP复制到剪切板,在记事本或其他类似工具记下来。
打开 VMProtect 3.4,点击需保护的进程->添加进程,然后把地址004016B0复制进去,编译类型选择超级(编译+虚拟)

点击选项,除了移除调试信息选“是”,其余都选“否”,因为这次测试只针对虚拟机保护,暂不研究其他内容。
然后点击编译,就可以得到加壳后的文件CrackMe.vmp.exe
用 LordPE 查看,CrackMe.vmp.exe多了一个区段.vmp0
4. 调试和脚本编写
4.1 调试准备
用 OD 加载CrackMe.vmp.exe,做好以下几个准备。
在调试选项中设置,因为记录需要跟踪的信息

打开 RUN 跟踪

点击菜单栏的
E,将显示的 DLL 标记为系统 DLL,以免之后调试跟踪( trace) 时被记录
4.2 调试过程
Ctrl + g 进入之前记下的地址004016B0,在此处 F2 下一个断点然后运行。可以看到,对比加壳前的情况,反汇编代码已经发生了很大变化。
Ctrl + F11 跟踪步过,程序也打印出了
Please enter your pwd >>

4.3 Trace 记录
在程序中随便输入一个密码,例如55,然后点开 OD 菜单栏中的……,点击右键选择 记录到文件,把调试跟踪的结果保存为 trace.txt,记得勾选“附加到已有文件”和“写入采集的数据”

查看trace.txt
在trace.txt搜索00000040,因为十六进制的00000040的二进制是0100 0000,可以被用来表示 ZF 标志位。在 x86 架构中,ZF 标志位的位置正好对应到 EFLAGS 寄存器的第 6 位(从第 0 位开始算起)。也就是说,当执行某些指令后,如果结果为零,则 ZF 标志位会被置 1。
VMP 会使用以下公式来计算 ZF 的值:
1 | zf = and(0x40, eflags) |
其中,0x40 就是 ZF 标志位的掩码值。当 eflags 寄存器的第 6 位为 1 时,and 运算的结果也会为 1,表示 ZF 标志位为 1。反之,当第 6 位为 0 时,and 运算的结果为 0,表示 ZF 标志位为 0。这样做可以非常高效地判断 ZF 标志位的状态,而不需要进行复杂的计算。
本次爆破测试是比较输入的密码是否是正确的密码,所以代码实现逻辑大概率依靠 CMP、JZ(若为 0 则跳转,ZF 需为 1)或者JE(若相等则跳转,ZF 需为 1)。
这类指令影响的符号位正是 ZF,所以需要搜寻00000040。
找到00000040后还需要在附近寻找类似以下特征的汇编指令
1 2 3 4 5 | not anot band a,b//这三行代码通常不是连续出现,中间会有无用的代码隔开//a, b皆为通用寄存器 |
寻找具备这种特征的汇编指令,是因为这是与非门(俗称万用门)的运算逻辑,表示为: Nand(a,b) = ~a & ~b,即将两个数分别取反后再进行与运算。汇编里面最基本的4种逻辑运算,都能用与非门表示。所以 VMP 3.x 版本中大量使用了Nand运算来表示其他的逻辑运算,真实地隐藏了原本的各种逻辑运算,有效地加大了逆向分析的难度。
1 2 3 4 | Not(a) = ~a = ~a & ~a = Nand(a,a)Or(a,b) = a | b = ~(~a & ~b) = Nand(Nand(a,b),Nand(a,b))And(a,b) = a & b = ~~a & ~~b = Nand(Nand(a,a),Nand(b,b))Xor(a,b) = (~a & b) | (a & ~b) = (0 | (a & ~b)) | (0 | (b & ~a)) = (a & (~a | ~b)) | (b & (~a | ~b)) = (~a | ~b) & (a | b) = ~(a & b) | ~(~a & ~b) = Nand(And(a,b),Nand(a,b)) =Nand(Nand(Nand(a,a),Nand(b,b)),Nand(a,b)) |
cmp指令本质上是减法,只不过结果不会写回操作数,Nand 门也能实现减法:
1 2 3 | -a = ~a+1 => ~a = -a -1~(~a+b) = ~(-a-1+b) = -(-a-1+b)-1 = a-b => a-b = Not(Not(a)+b)a-b最终可以由Not(Not(a)+b)来表示,而Not(a)又可以用Nand(a,a)来表示 |
简而言之,看到了not a, not b, and a,b这类特征的反汇编指令,就可以合理怀疑此处是 VMP 处理虚拟机指令的 handler 。VMP 会在 handler 入口处大量使用 nand 门运算来隐藏原本的各种逻辑运算。这些"Nand"指令往往会涉及对EFLAGS寄存器的读写操作。通过精细控制EFLAGS标志位的状态,VMP 可以实现对程序控制流的保护。
找到了疑似的指令,记下做 and 运算指令的地址0049B79E、以及两个寄存器的值ECX=00000040、EAX=00000246
1 | and ecx, eax//运算结果为0x40 |

(FL 是 Flags标志位的缩写,FL=0 意味着在执行指令and ecx, eax后没有发生任何标志位的改变。因为and ecx, eax执行结果为0x40,也就是说原本的 ZF 标志位就是 1。在后面的操作中,我们只需要将修改 ecx ,就能将 ZF 的标志位改为 0, 就能改变程序原本的跳转逻辑。例如,在输入错误时,原本打印“failed”,就能跳转到 “successful!“)
4.4 脚本编写
1 2 3 4 5 6 7 8 9 10 11 12 | bp 0049B79E //设置断点,当程序执行到地址 0049B79E 时暂停执行。start: //标签,用于标识后续指令的起始位置。run //继续执行程序,直到触发了设置的断点。cmp ecx, 00000040jnz startcmp eax, 00000246jnz startend:bc 0049B79E //清除断点,使得程序不再在地址 0049B79E 处暂停执行。ret //返回指令,结束当前脚本的执行。 |
保存为script.txt
5. 重新调试和破解
5.1 运行脚本
OD 重新加载CrackMe.vmp.exe,在反汇编窗口点击右键,选择运行脚本->script.txt
此时程序会运行,并打印字符串Please enter your pwd >>,随便输入 66 按回车键,会弹出窗口显示“脚本运行完成”。
5.2 修改寄存器的值
点击“确定”后,发现 EIP 停留在了0049B79E,因为之前脚本就在这个地址下了断点。注意此时的寄存器窗口,各个寄存器的值
1 2 3 4 5 6 7 8 9 | EAX 00000246ECX 00000040EDX 50655CD1EBX 004D7A92 CrackMe_.004D7A92ESP 0022FDE4EBP 0049B777 CrackMe_.0049B777ESI 0022FEC6EDI 004F9B18 CrackMe_.004F9B18EIP 0049B79E CrackMe_.0049B79E |

F7 走一下,寄存器的值没有发生任何变化
我们将 ECX 的值从00000040改为00000000
6. 破解成功
然后再点击运行,破解成功,注意此时的 ZF 标志位为 0;
1 2 3 | eax = 0x246;ecx = 0x0;运算结果是0; |

对比看一下,在输入错误密码且没有修改 ecx 的情况下,ZF 标志位为1
参考链接:
https://bbs.kanxue.com/thread-224732.htm
https://www.cnblogs.com/theseventhson/p/14274653.html
https://www.bilibili.com/video/BV1vK4y1Q7K7/?spm_id_from=333.337.search-card.all.click&vd_source=e5b65cf3bea873b0cfe83c6f3d30a710
《加密与解密(第4版)》