首页
社区
课程
招聘
[原创]VMProtect保护壳爆破步骤详解(入门级)
发表于: 2024-5-31 16:09 18564

[原创]VMProtect保护壳爆破步骤详解(入门级)

2024-5-31 16:09
18564

VMProtect 软件公司成立于2000年,总部位于俄罗斯叶卡捷琳堡。该公司出品的软件保护软件 VMProtect(目前版本已更新到 3.x,以下简称VMP)可以说是软件破解领域的圣杯,多年来无数逆向分析人员前赴后继,一直试图揭开 VMP 的神秘面纱。
VMP 的特色功能包括虚拟化、混淆、反调试等。本文的测试内容主要针对的是VMP的虚拟化机制,即VMP 可以将受保护的代码放到内置的虚拟机中运行,以防止反编译和破解。

VMP 代码虚拟机是一款栈机,也就是说,VMP 的代码虚拟机与被保护程序的代码处于栈空间中。基于栈的虚拟机指令集主要是通过操作栈顶元素来完成。例如,Java 虚拟机(JVM)就是一个典型的基于栈的虚拟机。与之相对应的还有基于寄存器的虚拟机。指令操作数存储在虚拟寄存器中,指令集直接对寄存器进行操作。Lua 的虚拟机(称为 Lua 虚拟机,LVM)就是基于寄存器的。
而基于栈的 VMP 虚拟机因为要维护出栈入栈,所以执行同样的操作,需要的指令较多,间接使得执行效率较差。 VMP 会让程序变得臃肿、运行速度变慢,但被保护的程序代码膨胀后,也能给逆向分析人员造成很多困扰。

(表格出自《加密与解密(第4版)》)
VMP 虚拟化流程:

系统环境:Windows 7 专业版(32 位)
IDE:Dev-C++ 5.7.1
加壳程序:VMProtect 3.4

编译得到CrackMe.exe

在输入错误密码的情况下,程序仍可打印“successful!”
(测试演示出自B站up主Rairn,文末有提供视频链接)

用 OD 加载CrackMe.exe,找到一个关键且易识别的位置,用 VMP 加密;在 OD 反汇编窗口点击右键,选择中文搜索引擎->智能搜索,搜索源代码中的字符串,例如successful
image.png
点进字符串successful对应的地址 0x00401702,分析一下反汇编代码,将加壳位置定为 0x004016B0,这看上去应该是一个函数的起始位置。将004016B0 55 PUSH EBP复制到剪切板,在记事本或其他类似工具记下来。
image.png
打开 VMProtect 3.4,点击需保护的进程->添加进程,然后把地址004016B0复制进去,编译类型选择超级(编译+虚拟)
image.png
image.png
点击选项,除了移除调试信息选“是”,其余都选“否”,因为这次测试只针对虚拟机保护,暂不研究其他内容。
image.png
然后点击编译,就可以得到加壳后的文件CrackMe.vmp.exe
image.png
用 LordPE 查看,CrackMe.vmp.exe多了一个区段.vmp0
image.png

用 OD 加载CrackMe.vmp.exe,做好以下几个准备。

在调试选项中设置,因为记录需要跟踪的信息
image.png

打开 RUN 跟踪

点击菜单栏的E,将显示的 DLL 标记为系统 DLL,以免之后调试跟踪( trace) 时被记录
image.png

Ctrl + g 进入之前记下的地址004016B0,在此处 F2 下一个断点然后运行。可以看到,对比加壳前的情况,反汇编代码已经发生了很大变化。
image.png
Ctrl + F11 跟踪步过,程序也打印出了

Please enter your pwd >>

image.png

在程序中随便输入一个密码,例如55,然后点开 OD 菜单栏中的……,点击右键选择 记录到文件,把调试跟踪的结果保存为 trace.txt,记得勾选“附加到已有文件”和“写入采集的数据”
image.png
image.png
查看trace.txt
image.png
trace.txt搜索00000040,因为十六进制的00000040的二进制是0100 0000,可以被用来表示 ZF 标志位。在 x86 架构中,ZF 标志位的位置正好对应到 EFLAGS 寄存器的第 6 位(从第 0 位开始算起)。也就是说,当执行某些指令后,如果结果为零,则 ZF 标志位会被置 1。
VMP 会使用以下公式来计算 ZF 的值:

其中,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后还需要在附近寻找类似以下特征的汇编指令

寻找具备这种特征的汇编指令,是因为这是与非门(俗称万用门)的运算逻辑,表示为: Nand(a,b) = ~a & ~b,即将两个数分别取反后再进行与运算。汇编里面最基本的4种逻辑运算,都能用与非门表示。所以 VMP 3.x 版本中大量使用了Nand运算来表示其他的逻辑运算,真实地隐藏了原本的各种逻辑运算,有效地加大了逆向分析的难度。

cmp指令本质上是减法,只不过结果不会写回操作数,Nand 门也能实现减法:

简而言之,看到了not a, not b, and a,b这类特征的反汇编指令,就可以合理怀疑此处是 VMP 处理虚拟机指令的 handler 。VMP 会在 handler 入口处大量使用 nand 门运算来隐藏原本的各种逻辑运算。这些"Nand"指令往往会涉及对EFLAGS寄存器的读写操作。通过精细控制EFLAGS标志位的状态,VMP 可以实现对程序控制流的保护。
找到了疑似的指令,记下做 and 运算指令的地址0049B79E、以及两个寄存器的值ECX=00000040EAX=00000246

image.png
(FL 是 Flags标志位的缩写,FL=0 意味着在执行指令and ecx, eax后没有发生任何标志位的改变。因为and ecx, eax执行结果为0x40,也就是说原本的 ZF 标志位就是 1。在后面的操作中,我们只需要将修改 ecx ,就能将 ZF 的标志位改为 0, 就能改变程序原本的跳转逻辑。例如,在输入错误时,原本打印“failed”,就能跳转到 “successful!“)

保存为script.txt

OD 重新加载CrackMe.vmp.exe,在反汇编窗口点击右键,选择运行脚本->script.txt
image.png
此时程序会运行,并打印字符串Please enter your pwd >>,随便输入 66 按回车键,会弹出窗口显示“脚本运行完成”。
image.png

点击“确定”后,发现 EIP 停留在了0049B79E,因为之前脚本就在这个地址下了断点。注意此时的寄存器窗口,各个寄存器的值

image.png
F7 走一下,寄存器的值没有发生任何变化
image.png
我们将 ECX 的值从00000040改为00000000
image.png

然后再点击运行,破解成功,注意此时的 ZF 标志位为 0;

image.png
对比看一下,在输入错误密码且没有修改 ecx 的情况下,ZF 标志位为1
image.png

参考链接:
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版)》

基本块名称 功能
VStartVM 从真实环境到虚拟环境的转换,开启新的虚拟空间给虚拟机使用,将除 esp 外的所有寄存器压入堆栈,esp 寄存器原来的值存放在 ebp 寄存器中
VMDispatcher 虚拟指令调度分流
VHandler1..N 不同功能的虚拟指令
VCheckESP 检查堆栈基本块,如果 esp (VMP 使用的堆栈)与 ebp (真实堆栈)即将被覆盖,则跟踪转到下一个基本块来开辟堆栈地址(sub esp, xxx),并将原来的 VMContext 中的内容复制到新的堆栈地址处,否则跳回 VMDispatcher
VRet 这个指令存在于 VHandler 集合中。这个指令将堆栈中压入的寄存器值还原到物理寄存器中,然后退出整个虚拟机环境,并切换回真实的 CPU 环境。注意:这个指令的作用是退出虚拟环境,并不等于汇编语义上的 retn。
#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;
}
#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;
}
zf = and(0x40, eflags)
zf = and(0x40, eflags)
not a
not b
and a,b
//这三行代码通常不是连续出现,中间会有无用的代码隔开
//a, b皆为通用寄存器
not a
not b
and a,b
//这三行代码通常不是连续出现,中间会有无用的代码隔开
//a, b皆为通用寄存器
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))
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))

[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)

最后于 2024-5-31 16:17 被ZyOrca编辑 ,原因:
上传的附件:
收藏
免费 8
支持
分享
最新回复 (6)
雪    币: 2575
活跃值: (502)
能力值: ( LV2,RANK:85 )
在线值:
发帖
回帖
粉丝
2
解决问题的思路值得学习
2024-5-31 18:25
0
雪    币: 1119
活跃值: (4192)
能力值: ( LV5,RANK:69 )
在线值:
发帖
回帖
粉丝
3
能保存修改的文件并成功运行就更厉害了
2024-5-31 18:59
0
雪    币: 2445
活跃值: (230)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
4
值得学习,希望多出这种零基础的教程!!
2024-6-3 07:08
0
雪    币: 15
活跃值: (383)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
5
这个特征太明显了,已经用泄露的源码把这段魔改了
2024-6-5 19:11
0
雪    币: 4224
活跃值: (788)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
6
值得学习,过程思路清晰可见,感谢~~
2024-6-24 10:24
0
雪    币: 4714
活跃值: (4250)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
7
vmp也是一顿被吹上神探,基本上只要是虚拟化的加壳软件,有哪个是能被轻易分析的像tmd什么的,加了壳后都一样丢回收站。
2024-6-24 13:45
1
游客
登录 | 注册 方可回帖
返回
//