VMProtect 代码虚拟化保护详解
一、核心原理
虚拟化保护 = 自定义虚拟机 + 字节码转换
text
原始 x86/x64 指令
↓
VMProtect 转换
↓
自定义虚拟机字节码(VM Bytecode)
↓
运行时解释执行
二、具体示例对比
📌 示例 1:简单加法运算
原始 C 代码
C
int add(int a, int b) {
return a + b;
}
未保护的汇编代码
asm
; 标准 x86 汇编
push ebp
mov ebp, esp
mov eax, [ebp+8] ; 获取参数 a
add eax, [ebp+12] ; 加上参数 b
pop ebp
ret
指令数:6 条 | 可读性:高 | 容易破解:✅
VMProtect 虚拟化后
asm
; 进入虚拟机
call VM_Entry_0x4A3F10
VM_Entry_0x4A3F10:
push ebp
mov ebp, esp
sub esp, 200h ; 分配虚拟机栈空间
; 初始化虚拟 CPU 上下文
lea esi, [VM_Context]
mov [esi+0], eax ; 保存真实寄存器
mov [esi+4], ebx
mov [esi+8], ecx
; ... 保存所有寄存器
; 加载虚拟指令流
mov edi, VM_Bytecode_12A4 ; 虚拟指令地址
VM_Dispatcher:
; 取虚拟指令
movzx eax, byte ptr [edi]
inc edi
; 跳转到 Handler
shl eax, 2
jmp dword ptr [VM_Handler_Table + eax]
; Handler 1: 虚拟 PUSH
VM_Handler_0x23:
mov eax, [edi]
add edi, 4
mov ebx, [esi+VM_SP]
mov [ebx], eax
add [esi+VM_SP], 4
jmp VM_Dispatcher
; Handler 2: 虚拟 ADD
VM_Handler_0x47:
mov ebx, [esi+VM_SP]
sub ebx, 4
mov eax, [ebx] ; 栈顶元素
sub ebx, 4
add eax, [ebx] ; 与次栈顶相加
mov [ebx], eax
sub [esi+VM_SP], 4
jmp VM_Dispatcher
; Handler 3: 虚拟 RET
VM_Handler_0x91:
mov eax, [esi+VM_Ret]
; 恢复寄存器
mov ebx, [esi+4]
mov ecx, [esi+8]
; ...
leave
ret
; 虚拟指令字节码(加密存储)
VM_Bytecode_12A4:
db 23h, 08h, 00h, 00h, 00h ; PUSH [ebp+8]
db 23h, 0Ch, 00h, 00h, 00h ; PUSH [ebp+12]
db 47h ; ADD
db 91h ; RET
指令数:200+ 条 | 可读性:极低 | 容易破解:❌
三、实际案例分析
📌 示例 2:字符串比较
原始代码
C
if (strcmp(password, "Admin123") == 0) {
return true;
}
未保护汇编
asm
push offset aAdmin123 ; "Admin123"
push [ebp+password]
call _strcmp
add esp, 8
test eax, eax
jz Success
VMProtect 虚拟化后的特征
asm
; 1. 字符串被加密分散存储
VM_String_Chunk_1: db 0x7A, 0x3F, 0x91 ; 'A' 加密
VM_String_Chunk_2: db 0x2C, 0x88, 0x45 ; 'd' 加密
; ...
; 2. 比较逻辑被拆分成上百个虚拟指令
call VM_Entry_String_Compare
; 内部执行:
; - 动态解密字符串
; - 逐字节虚拟比较
; - 混淆控制流
; - 垃圾指令干扰
四、虚拟机架构详解
VMProtect 虚拟 CPU 结构
text
┌─────────────────────────────────┐
│ 虚拟机上下文 (VM Context) │
├─────────────────────────────────┤
│ vEAX, vEBX, vECX, vEDX │ 虚拟寄存器
│ vESP, vEBP, vESI, vEDI │
│ vEIP (虚拟指令指针) │
│ vEFLAGS (虚拟标志位) │
│ Virtual Stack (虚拟栈) │
└─────────────────────────────────┘
↓
┌─────────────────────────────────┐
│ 虚拟指令集 (150+ 指令) │
├─────────────────────────────────┤
│ 0x01 → VM_PUSH │
│ 0x02 → VM_POP │
│ 0x15 → VM_ADD │
│ 0x23 → VM_XOR │
│ 0x47 → VM_JMP │
│ ... → 每个程序指令集不同 │
└─────────────────────────────────┘
五、实战反虚拟化难点
🔥 挑战 1:动态指令集
text
程序 A 的虚拟指令 0x23 = ADD
程序 B 的虚拟指令 0x23 = XOR
↓
每次编译随机生成,无法通用破解
🔥 挑战 2:多层嵌套
asm
真实代码
↓ VMProtect 第一层
虚拟代码 A
↓ VMProtect 第二层
虚拟代码 B (虚拟机中的虚拟机)
↓
可达 3-5 层嵌套
🔥 挑战 3:变异 Handlers
asm
; 同一个 ADD 操作的 5 种不同实现
VM_ADD_Variant_1:
mov eax, [vStack]
add eax, [vStack+4]
VM_ADD_Variant_2:
xor eax, eax
sub eax, [vStack]
neg eax
add eax, [vStack+4]
; 运行时随机选择
六、真实程序对比
未保护程序(IDA 反编译)
C
// 清晰可读
int __cdecl check_license(char *key) {
if ( strlen(key) == 16 ) {
return validate_format(key);
}
return 0;
}
VMProtect 保护后(IDA 显示)
asm
; 完全无法识别逻辑
seg000:004A3F10 call sub_4A8E20
seg000:004A3F15 db 89h, 0C4h, 83h, 0ECh, 64h
seg000:004A3F1A db 0E8h, 3Fh, 12h, 0, 0
seg000:004A3F1F ; --------------- 乱码区域 ---------------
seg000:004A3F1F db 156 dup(?)
; F5 伪代码显示:无法反编译
七、虚拟化保护等级
| 等级 | 性能损耗 | 保护强度 | 适用场景 |
|---|---|---|---|
| 轻度 | 2-5x | ⭐⭐⭐ | 注册验证函数 |
| 中度 | 10-20x | ⭐⭐⭐⭐ | 核心算法 |
| 重度 | 50-100x | ⭐⭐⭐⭐⭐ | 关键加解密 |
| Ultra | 200x+ | 💀 | 极端保护(少用) |
八、检测虚拟化保护的方法
使用工具识别
Bash
# 1. Detect It Easy
"VMProtect 3.5.1 [VM + Mutation]"
# 2. IDA Pro 特征
- 大量间接跳转 (jmp [eax*4 + table])
- 无法识别的函数调用
- .vmp0 / .vmp1 段
# 3. 动态特征
- 单步执行极慢
- 大量 PUSHAD/POPAD
- 频繁栈操作
九、反虚拟化思路(研究用途)
方法 1:虚拟机跟踪
Python
# 记录虚拟指令执行流
vm_trace = []
while in_vm:
opcode = read_byte(vEIP)
vm_trace.append({
'opcode': opcode,
'handler': get_handler(opcode),
'context': dump_registers()
})
方法 2:符号执行
text
使用 Triton/Miasm 等框架
自动化提取虚拟指令语义
重建原始逻辑
方法 3:Devirtualization 工具
- VMPTrace (自动跟踪)
- NoVMP (部分版本可用)
- 手工分析 (最可靠但最难)
十、总结
VMProtect 虚拟化保护的本质:
text
将 1 行简单代码
转换为 1000 行混淆代码
运行在自定义 CPU 上
防御建议:
- ✅ 只保护核心 5% 的代码
- ✅ 配合其他保护(反调试、反注入)
- ❌ 不要全程序虚拟化(性能崩溃)
破解难度:⭐⭐⭐⭐⭐ (顶级)
普通逆向工程师需要:200-500 小时
即使破解也需要深厚的汇编和虚拟机知识
IDA 中识别 VM 虚拟化代码完全指南
一、快速判断清单 ✅
text
✓ F5 无法反编译或显示垃圾代码
✓ 控制流图异常复杂/完全平坦
✓ 大量间接跳转和查表操作
✓ 无意义的重复指令模式
✓ 无法识别函数边界
✓ 交叉引用异常(XRef 混乱)
二、静态特征对比
📌 特征 1:F5 伪代码对比
✅ 正常代码(未虚拟化)
C
// IDA F5 反编译结果清晰
int __cdecl check_password(const char *input)
{
int result;
if ( strlen(input) == 8 )
{
result = 1;
if ( strcmp(input, "Admin123") )
result = 0;
}
else
{
result = 0;
}
return result;
}
❌ VM 虚拟化代码
C
// F5 显示以下情况之一:
// 情况 1:完全无法反编译
// Could not generate function code for sub_401000
// The function is too complex or contains invalid code
// 情况 2:显示乱码
void __cdecl sub_401000()
{
__asm { jmp rax } // 全是汇编片段
}
// 情况 3:巨大的垃圾代码
int sub_401000()
{
char v0; char v1; char v2; // 数百个变量
// ... 1000+ 行无意义操作
return ((v47 ^ v23) & (v88 | v12)) + v156;
}
📌 特征 2:汇编代码模式
✅ 正常函数汇编
asm
sub_401000 proc near
push ebp
mov ebp, esp
sub esp, 10h
mov eax, [ebp+arg_0]
test eax, eax
jz short loc_401020
call sub_402000
add esp, 10h
pop ebp
retn
sub_401000 endp
特点:结构清晰、有明确的函数序言/结尾
❌ VM 虚拟化汇编(典型模式)
模式 A:VM 入口特征
asm
; ========== VM 入口标志 ==========
sub_401000 proc near
pushad ; 保存所有寄存器
pushfd ; 保存标志位
mov esi, offset vm_context
mov [esi], eax ; 保存上下文
mov [esi+4], ebx
mov [esi+8], ecx
; ... 保存所有寄存器到 VM 上下文
lea edi, vm_bytecode_start
jmp vm_dispatcher ; 跳转到 VM 调度器
模式 B:VM 调度器(Dispatcher)
asm
vm_dispatcher:
movzx eax, byte ptr [edi] ; 读取虚拟指令
inc edi ; 移动虚拟 IP
; 关键特征:查表跳转
lea ebx, handler_table
shl eax, 2 ; eax * 4
add ebx, eax
jmp dword ptr [ebx] ; 间接跳转到 Handler
; 或者更复杂的变种:
mov ecx, [handler_table + eax*4]
xor ecx, 0x12345678 ; 解密 Handler 地址
push ecx
ret ; 伪装的跳转
模式 C:VM Handler 碎片
asm
; Handler 通常短小且重复模式
handler_0x23:
mov eax, [esi+vm_stack_ptr]
mov ebx, [edi] ; 读取操作数
add edi, 4
mov [eax], ebx
add [esi+vm_stack_ptr], 4
jmp vm_dispatcher ; 回到调度器
handler_0x47:
mov eax, [esi+vm_stack_ptr]
sub eax, 4
mov ebx, [eax]
sub eax, 4
add [eax], ebx
sub [esi+vm_stack_ptr], 4
jmp vm_dispatcher ; 每个都跳回
📌 特征 3:控制流图(CFG)对比
✅ 正常函数的 CFG(IDA Graph View)
text
[入口]
↓
[参数检查]
↙ ↘
[错误] [继续]
↓
[处理]
↓
[返回]
特点:树状结构,逻辑清晰,深度 3-10 层
❌ VM 虚拟化的 CFG
类型 1:平坦化控制流
text
[入口]
↓
[Dispatcher] ←───┐
↓ ↓ ↓ ↓ ↓ │
A B C D E │
└─┴─┴─┴─┴────────┘
IDA 显示:所有基本块都连到同一个 Dispatcher
类型 2:意大利面条式
text
极度混乱的交叉跳转
┌─→ [A] ←──┐
│ ↓ │
│ [B] ─→ [C]
│ ↓ ↖ ↓
└─ [D] ─→ [E] ←┐
↓ │
[F] ────────┘
特点:基本块数量 200+,连接混乱
类型 3:完全线性(高度混淆)
text
[Block 1] → [Block 2] → [Block 3] → ... → [Block 500]
每个块只有 2-5 条指令,全是垃圾操作
三、IDA 实战识别步骤
🔍 步骤 1:检查函数列表
text
IDA View → Functions Window (Shift+F3)
正常程序:
✓ 函数名可识别 (sub_401000, CheckLicense)
✓ 函数大小合理 (100-2000 字节)
✓ 大部分函数可以 F5
VM 保护程序:
✗ 出现超大函数 (20KB-200KB)
✗ 函数名全是 sub_xxxxxx
✗ 大量红色标记(IDA 无法分析)
✗ 函数重叠(地址混乱)
🔍 步骤 2:查看字符串交叉引用
asm
// 正常代码:
.rdata:00405000 aAdmin123 db 'Admin123',0
// 交叉引用清晰:
sub_401000:
push offset aAdmin123 ; ← 一个明确的引用
call strcmp
// =====================================
// VM 代码:
.vmp1:00501234 db 0x7A, 0x3F, 0x91, ... ; 加密字符串
// 交叉引用混乱:
sub_401000:
call vm_entry
; ← 字符串被隐藏在 VM 内部
; IDA 显示:无交叉引用
判断方法:
text
Strings Window (Shift+F12) → 双击字符串 → 查看 XRef
正常:跳转到明确的 push/mov 指令
VM: 跳转到 .vmp 段或显示无引用
🔍 步骤 3:检查 Hex-Rays 警告
按 F5 后查看输出窗口:
text
✅ 正常情况:
// 无警告或少量警告
sp-analysis failed
// 可以忽略
❌ VM 虚拟化:
// 大量严重警告
positive sp value has been found
too big function, not all data is shown
prolog analysis failed
inconsistent stack layout
the graph is too big (>1000 blocks)
🔍 步骤 4:识别 VM 关键结构
在 IDA 中搜索以下模式:
搜索 1:Handler Table(处理器表)
text
IDA → Search → Sequence of bytes
搜索模式:FF 24 85 [?? ?? ?? ??]
找到:
jmp ds:off_5012A0[eax*4] ; ← 这是 Handler 跳转表
双击 off_5012A0:
dd offset handler_0
dd offset handler_1
dd offset handler_2
...
dd offset handler_255 ; ← 通常有 100-300 个 Handler
搜索 2:PUSHAD/POPAD 模式
text
Alt+T (文本搜索) → 输入 "pushad"
VM 入口通常:
pushad
call $+5
pop ebp
sub ebp, xxxx
搜索 3:大量 XOR 解密循环
asm
; VM 解密虚拟指令的特征
loc_loop:
mov al, [edi]
xor al, [esi]
rol al, 3
mov [edi], al
inc edi
inc esi
loop loc_loop
四、具体代码对比示例
📊 案例:注册码验证函数
原始 C 代码
C
bool validate_serial(char* serial) {
if (strlen(serial) != 16) return false;
int sum = 0;
for (int i = 0; i < 16; i++) {
sum += serial[i];
}
return (sum == 0x5A3);
}
✅ 未保护的汇编(IDA 显示)
asm
validate_serial proc near
push ebp
mov ebp, esp
push edi
mov edi, [ebp+serial]
push edi
call _strlen
cmp eax, 10h ; 比较长度
jnz short fail
xor eax, eax ; sum = 0
xor ecx, ecx ; i = 0
loop_start:
movsx edx, byte ptr [edi+ecx]
add eax, edx
inc ecx
cmp ecx, 10h
jl short loop_start
cmp eax, 5A3h ; 关键比较
setz al
pop edi
pop ebp
retn
fail:
xor eax, eax
pop edi
pop ebp
retn
validate_serial endp
特征:
- 代码行数:~30 行
- 基本块:6 个
- F5 完美还原
❌ VMProtect 虚拟化后(IDA 显示)
asm
validate_serial proc near
; ===== VM 入口 =====
push 0
pushfd
pushad
call $+5
pop ebp
sub ebp, 1A2B3C4Dh ; 计算 VM 基址
; 初始化 VM 上下文(200+ 行)
lea esi, [ebp+12A40h] ; VM Context
mov [esi+VM_EAX], eax
mov [esi+VM_EBX], ebx
; ... 50+ 行寄存器保存
; 加载加密的虚拟指令
lea edi, [ebp+45A20h] ; 虚拟字节码
mov ecx, 1A3Fh
decrypt_loop:
mov al, [edi]
xor al, 5Ah
ror al, 3
xor al, cl
mov [edi], al
inc edi
loop decrypt_loop
; ===== VM 调度器 =====
vm_dispatch_45A20:
movzx eax, byte ptr [ebp+edi]
inc edi
; 混淆的 Handler 查表
mov ebx, eax
shl ebx, 2
xor ebx, 7A3F12B4h
lea ecx, [ebp+handler_table]
mov ecx, [ecx+ebx]
xor ecx, ebp
push ecx
ret ; 跳转到 Handler
; ===== Handler 碎片 (100+ 个) =====
handler_23A4:
; 虚拟 PUSH 操作
mov eax, [ebp+edi]
add edi, 4
mov ebx, [esi+VM_SP]
mov [ebx], eax
add dword ptr [esi+VM_SP], 4
jmp vm_dispatch_45A20
handler_7F12:
; 虚拟 ADD 操作
mov eax, [esi+VM_SP]
sub eax, 4
mov ebx, [eax]
sub eax, 4
add [eax], ebx
sub dword ptr [esi+VM_SP], 4
jmp vm_dispatch_45A20
; ... 重复 100+ 个类似 Handler
; ===== VM 退出 =====
vm_exit:
mov eax, [esi+VM_EAX]
mov ebx, [esi+VM_EBX]
; ... 50+ 行恢复寄存器
popad
popfd
retn
validate_serial endp
特征:
- 代码行数:3000+ 行
- 基本块:200+ 个
- F5 显示:
// Could not generate code
五、IDA 插件辅助识别
🔌 推荐插件
1. VMProtect Analyzer
Python
# 自动识别 VM 结构
安装后右键 → VMProtect → Analyze VM
输出:
[+] VM Entry found at 0x401000
[+] Dispatcher at 0x4012A0
[+] Handler Table: 0x5012A0 (156 handlers)
[+] VM Context size: 0x200
2. IDA Denigma
text
查看 → Open Subviews → Denigma
会标注:
🔴 Highly obfuscated (VM suspected)
🟡 Control flow flattening detected
3. 自定义 IDAPython 脚本
Python
# 检测 VM 特征
import idaapi
import idc
def detect_vm():
ea = idc.here()
# 检测 1:基本块数量
func = idaapi.get_func(ea)
cfg = idaapi.FlowChart(func)
block_count = 0
for block in cfg:
block_count += 1
if block_count > 100:
print("[!] Suspicious: {} basic blocks".format(block_count))
# 检测 2:间接跳转
indirect_jmp = 0
for head in idautils.Heads(func.start_ea, func.end_ea):
if idc.print_insn_mnem(head) in ["jmp", "call"]:
if idc.get_operand_type(head, 0) != idc.o_near:
indirect_jmp += 1
if indirect_jmp > 10:
print("[!] Suspicious: {} indirect jumps".format(indirect_jmp))
# 检测 3:PUSHAD/POPAD 配对
pushad_count = 0
first_insn = idc.print_insn_mnem(func.start_ea)
if first_insn == "pushad":
print("[!] VM Entry pattern detected")
detect_vm()
六、快速判断流程图
text
┌─────────────────────┐
│ 选中可疑函数/代码 │
└──────────┬──────────┘
↓
┌──────────┐
│ 按 F5 │
└─────┬────┘
↓
能反编译?
↙ ↘
YES NO ──→ 90% 是 VM
↓
代码是否异常复杂?
(1000+ 行无意义运算)
↓
YES ──→ 80% 是 VM
↓
NO
↓
查看 Graph View (空格键)
↓
基本块 > 100 个?
平坦化结构?
↓
YES ──→ 70% 是 VM
↓
NO
↓
搜索 "jmp [eax*4+table]"
↓
找到?
↓
YES ──→ 95% 是 VM
↓
NO ──→ 可能不是 VM
七、实战练习
🎯 在你的程序中测试
text
1. 打开 IDA Functions 窗口
2. 排序 "Size" 列,找最大的函数
3. 双击进入 → 按 F5
4. 看到以下任一情况即为 VM:
- "positive sp value"
- 无法反编译
- 代码全是 __asm { }
5. 按空格查看图形视图
- 平坦化 → VM
- 超过 100 个基本块 → 可能 VM
八、总结对照表
| 特征 | 正常代码 | VM 虚拟化 |
|---|---|---|
| F5 反编译 | ✅ 成功 | ❌ 失败/乱码 |
| 基本块数量 | 5-50 | 100-1000+ |
| 代码行数 | 50-500 | 2000-50000 |
| 间接跳转 | 0-5 | 20+ |
| 函数大小 | <10KB | 20KB-200KB |
| 控制流 | 树状 | 平坦/意大利面 |
| PUSHAD | 少见 | 入口必有 |
| 字符串引用 | 清晰 | 隐藏/加密 |
九、典型误判情况
⚠️ 不是 VM 但看起来像:
- 编译器优化过度
C
// -O3 优化 + 内联展开
可能产生复杂代码,但:
✓ F5 仍能反编译
✓ 有明确的函数调用
- 手写汇编优化
asm
; 加密算法常用查表
movzx eax, byte ptr [esi]
mov al, [sbox + eax] ; ← 不是 VM
- 控制流平坦化(单独使用)
text
没有 VM 的完整特征:
- 缺少 VM Context
- 缺少 Handler 碎片
- F5 可以反编译(虽然难看)
🎓 最终建议
判断优先级(重要性排序):
- F5 无法反编译 → 最强信号
- 存在 Handler 跳转表 → 确认 VM
- 平坦化控制流 → 高度怀疑
- PUSHAD 入口模式 → 强烈怀疑
- 超大函数 (>20KB) → 需进一步分析
实战口诀:
text
F5 不行先怀疑
图形混乱要警惕
间接跳转是核心
Handler 表敲定锤
需要我演示分析具体的函数地址吗?提供你 IDA 中的可疑函数地址,我可以帮你详细判断!
VMProtect 实战脱壳案例详解
案例一:简单注册验证函数脱 VM(入门级)
📋 目标信息
text
程序:KeyGen.exe
保护:VMProtect 3.0.9 (轻度虚拟化)
目标:还原注册码验证逻辑
难度:⭐⭐
🔍 第一步:定位 VM 入口
IDA 分析
asm
.text:00401000 ; =============== 正常函数调用 ===============
.text:00401000 CheckSerial:
.text:00401000 push ebp
.text:00401001 mov ebp, esp
.text:00401003 push [ebp+serial]
.text:00401006 call VM_Entry_4A2000 ; ← 跳转到 VM
.text:0040100B test al, al
.text:0040100D jz ShowError
.text:00401013 call ShowSuccess
.text:00401018 retn
; =============== VM 入口 ===============
.vmp0:004A2000 VM_Entry_4A2000:
.vmp0:004A2000 pushad ; 保存所有寄存器
.vmp0:004A2001 pushfd
.vmp0:004A2002 call $+5
.vmp0:004A2007 pop ebp
.vmp0:004A2008 sub ebp, 4A2007h ; 计算相对基址
.vmp0:004A200E ; ... VM 初始化代码
🛠️ 第二步:动态跟踪 VM(使用 x64dbg + ScyllaHide)
配置环境
text
1. x64dbg 加载 KeyGen.exe
2. 插件 → ScyllaHide → 启用全部反调试对抗
3. 插件 → Trace Record → 开启指令跟踪
设置断点
text
断点1:004A2000 (VM 入口)
断点2:0040100B (VM 返回后)
运行 → 输入测试序列号 "AAAA-BBBB-CCCC-DDDD"
跟踪记录
text
[x64dbg 命令窗口]
> TraceIntoConditional [eip] > 004A2000 && [eip] < 004B0000
> run
[跟踪结果] trace_vm.txt
004A2000 | pushad
004A2001 | pushfd
004A2002 | call 004A2007
004A2007 | pop ebp
...
004A2103 | movzx eax, byte [edi] ; ← 读取虚拟指令
004A2106 | inc edi
004A2107 | lea ebx, [ebp+12A40]
004A210D | jmp [ebx+eax*4] ; ← Dispatcher 跳转
004A2110 | ; Handler 0x23
004A2110 | mov ecx, [edi]
004A2113 | add edi, 4
004A2116 | push ecx ; ← 虚拟 PUSH
004A2117 | jmp 004A2103 ; 回到 Dispatcher
...
🧠 第三步:识别虚拟指令
使用 VMPTrace 工具(自动化)
Bash
# 下载:https://github.com/0xnobody/vmpdump
vmptrace.exe -p KeyGen.exe -a 0x4A2000
[输出]
VM Entry: 0x004A2000
Bytecode Start: 0x004A3F20
Handler Table: 0x004A512A (84 handlers)
Extracted Virtual Instructions:
Offset Opcode Mnemonic Operand
------ ------ -------- -------
0x0000 23 VM_PUSH_IMM 0x10
0x0005 47 VM_PUSH_REG EBP+8
0x0007 A3 VM_CALL strlen
0x000C 12 VM_CMP_IMM 0x13
0x0011 E7 VM_JNZ 0x0045
0x0013 23 VM_PUSH_IMM offset aAdmin
0x0018 47 VM_PUSH_REG EBP+8
0x001A A3 VM_CALL strcmp
0x001F 56 VM_TEST_EAX
0x0020 E8 VM_JZ 0x0034
0x0022 23 VM_PUSH_IMM 0
0x0027 91 VM_RET
...
✍️ 第四步:手工还原(关键部分)
分析虚拟指令流
Python
# 根据 VMPTrace 输出还原逻辑
VM Code:
0x0000 VM_PUSH_IMM 0x10 # push 16
0x0005 VM_PUSH_REG [EBP+8] # push [ebp+8] (serial参数)
0x0007 VM_CALL strlen # call strlen
0x000C VM_CMP_IMM 0x13 # cmp eax, 19
0x0011 VM_JNZ 0x0045 # jnz fail
还原为 C 代码:
if (strlen(serial) != 19) {
goto fail;
}
继续分析:
0x0013 VM_PUSH_IMM offset aAdmin # push "ADMIN-"
0x0018 VM_PUSH_REG [EBP+8] # push serial
0x001A VM_CALL strncmp # call strncmp(serial, "ADMIN-", 6)
0x001F VM_TEST_EAX # test eax, eax
0x0020 VM_JZ 0x0034 # jz continue
还原为:
if (strncmp(serial, "ADMIN-", 6) != 0) {
return false;
}
完整还原代码
C
// 原始被虚拟化的函数(还原结果)
bool CheckSerial(char* serial) {
// 检查长度
if (strlen(serial) != 19) {
return false;
}
// 检查前缀
if (strncmp(serial, "ADMIN-", 6) != 0) {
return false;
}
// 检查校验和
int sum = 0;
for (int i = 6; i < 19; i++) {
if (serial[i] == '-') continue;
sum += serial[i];
}
// 关键算法(从 VM 中提取)
int expected = 0x4A3F;
if (sum != expected) {
return false;
}
return true;
}
// 有效序列号:ADMIN-XXXX-XXXX-XX(需满足校验和)
🎯 第五步:验证与 Patch
方法1:重写汇编
asm
; 用 IDA Keypatch 插件替换 VM 入口
.text:00401006 call VM_Entry_4A2000
替换为:
.text:00401006 call NewCheckSerial ; 跳转到我们重写的函数
.text:0040100B nop
; 在空白区域写入还原的代码
.text:00405000 NewCheckSerial:
.text:00405000 push ebp
.text:00405001 mov ebp, esp
.text:00405003 mov edi, [ebp+8] ; serial
.text:00405006 push edi
.text:00405007 call strlen
.text:0040500C cmp eax, 13h
.text:0040500F jnz short fail
.text:00405011 ; ... 继续实现逻辑
方法2:内存 Patch
Python
# 使用 Python + pefile 直接修改
import pefile
pe = pefile.PE('KeyGen.exe')
# 找到 VM 入口调用
vm_call_offset = 0x1006 # call VM_Entry_4A2000
original = b'\xE8\xF5\x0F\x0A\x00' # call 0x4A2000
# 替换为简单的逻辑
patch = b'\xB0\x01' # mov al, 1 (永远返回 true)
patch += b'\x90' * 3 # nop padding
pe.set_bytes_at_offset(vm_call_offset, patch)
pe.write('KeyGen_cracked.exe')
案例二:复杂算法脱 VM(进阶级)
📋 目标信息
text
程序:CryptoTool.exe
保护:VMProtect 3.5.1 (重度虚拟化 + Mutation)
目标:还原 AES 加密实现
难度:⭐⭐⭐⭐
🔬 挑战分析
VM 复杂度对比
text
简单 VM (案例一) 复杂 VM (案例二)
─────────────────── ───────────────────
84 个 Handlers → 247 个 Handlers
单层虚拟化 → 三层嵌套 VM
固定指令集 → 多态 Handlers
无混淆 → 重度 Mutation
IDA 显示
asm
.vmp1:005A3000 AES_Encrypt_VM:
.vmp1:005A3000 pushad
.vmp1:005A3001 call VM_Layer1_Init
.vmp1:005A3006 ; ... 3000+ 行初始化代码
.vmp1:005A7F23 call VM_Dispatcher_L1
.vmp1:005A7F28 ; ← IDA 标记:sp-analysis failed
; F5 反编译结果:
// Function chunk at 005B2000 size 0002A3F0 bytes
// ****** TOO COMPLEX TO DECOMPILE ******
🛠️ 进阶方法:符号执行(使用 Triton)
环境配置
Bash
# 安装 Triton 框架
pip install triton-library
# 准备 Pin Tool
git clone https://github.com/JonathanSalwan/Triton
cd Triton/pin
符号执行脚本
Python
#!/usr/bin/env python3
from triton import *
import sys
# 初始化 Triton
ctx = TritonContext()
ctx.setArchitecture(ARCH.X86_64)
ctx.setMode(MODE.ALIGNED_MEMORY, True)
# 加载程序
import lief
binary = lief.parse("CryptoTool.exe")
# 设置符号化输入
def symbolize_input():
# 将 AES 输入设为符号变量
input_addr = 0x7FFE0000
for i in range(16): # 16 字节输入
ctx.symbolizeMemory(MemoryAccess(input_addr + i, 1))
# 将 AES 密钥设为符号变量
key_addr = 0x7FFE1000
for i in range(16):
ctx.symbolizeMemory(MemoryAccess(key_addr + i, 1))
# Hook VM 执行
vm_entry = 0x5A3000
vm_exit = 0x5A9F20
trace_log = []
def instruction_callback(inst):
ctx.processing(inst)
# 记录关键操作
if inst.getType() == OPCODE.X86.XOR:
trace_log.append({
'addr': inst.getAddress(),
'asm': str(inst),
'symbolic': inst.getSymbolicExpressions()
})
# 检测 AES S-Box 查表
if 'movzx' in str(inst) and 'byte ptr' in str(inst):
# 可能是 S-Box 操作
trace_log.append({
'type': 'SBOX',
'addr': inst.getAddress()
})
# 执行并收集约束
def run_vm_trace():
# 从 VM 入口开始执行
rip = vm_entry
while rip != vm_exit:
# 获取指令
opcodes = getConcreteMemoryAreaValue(rip, 16)
inst = Instruction()
inst.setOpcodes(opcodes)
inst.setAddress(rip)
# 反汇编
ctx.disassembly(inst)
# 执行回调
instruction_callback(inst)
# 继续执行
ctx.processing(inst)
rip = ctx.getConcreteRegisterValue(ctx.registers.rip)
symbolize_input()
run_vm_trace()
# 分析收集的数据
print("[+] Collected {} operations".format(len(trace_log)))
# 识别 AES 模式
sbox_accesses = [x for x in trace_log if x.get('type') == 'SBOX']
print("[+] Found {} S-Box lookups (AES rounds)".format(len(sbox_accesses)))
🎯 关键突破:识别 AES 特征
从符号执行结果识别
Python
# 分析输出
[+] Collected 47293 operations
[+] Found 160 S-Box lookups
# 160 次 S-Box = 10 轮 AES-128
# (每轮 16 次,共 10 轮)
# 提取关键地址
S-Box Table: 0x005C2A40
Round Keys: 0x005C3100 - 0x005C31A0 (176 bytes)
MixColumns: Implemented as multiplication (not table)
验证发现
Python
# 在 IDA 中跳转到 S-Box 地址
# .vmp1:005C2A40
db 63h, 7Ch, 77h, 7Bh, 0F2h, 06Bh, 06Fh, 0C5h
db 30h, 01h, 67h, 02Bh, 0FEh, 0D7h, 0ABh, 76h
; ... 标准 AES S-Box!
# 提取到 Python
aes_sbox = [
0x63, 0x7C, 0x77, 0x7B, 0xF2, 0x6B, 0x6F, 0xC5,
# ... 完整 256 字节
]
# 与标准 AES 对比
from Crypto.Cipher import AES
import binascii
# 完全匹配!确认是标准 AES-128
📝 第三步:重建算法
从 VM 中提取的完整实现
Python
# 基于跟踪数据重建
class VM_AES_Recovered:
def __init__(self, key):
self.key = key
self.round_keys = self.key_expansion(key)
def encrypt(self, plaintext):
state = list(plaintext)
# Initial Round
state = self.add_round_key(state, self.round_keys[0])
# 9 Main Rounds
for round in range(1, 10):
state = self.sub_bytes(state)
state = self.shift_rows(state)
state = self.mix_columns(state)
state = self.add_round_key(state, self.round_keys[round])
# Final Round
state = self.sub_bytes(state)
state = self.shift_rows(state)
state = self.add_round_key(state, self.round_keys[10])
return bytes(state)
def sub_bytes(self, state):
# 从 VM 中提取的 S-Box(地址 0x005C2A40)
sbox = [0x63, 0x7C, 0x77, ...] # 完整 256 字节
return [sbox[b] for b in state]
# ... 其他函数实现
# 验证
test_key = b'\x00' * 16
test_plain = b'\x00' * 16
# VM 版本
vm_aes = VM_AES_Recovered(test_key)
vm_result = vm_aes.encrypt(test_plain)
# 标准 AES
from Crypto.Cipher import AES
cipher = AES.new(test_key, AES.MODE_ECB)
std_result = cipher.encrypt(test_plain)
assert vm_result == std_result # ✅ 验证通过!
🔨 第四步:替换实现
使用 DLL 劫持
C++
// AESProxy.dll - 替换虚拟化函数
#include <windows.h>
#include <openssl/aes.h>
// 原始 VM 函数签名(通过动态分析获得)
typedef void (*VM_AES_Encrypt_t)(
unsigned char* output,
const unsigned char* input,
const unsigned char* key
);
// 我们的实现
extern "C" __declspec(dllexport)
void VM_AES_Encrypt(
unsigned char* output,
const unsigned char* input,
const unsigned char* key
) {
// 使用标准 AES 替代 VM 实现
AES_KEY aes_key;
AES_set_encrypt_key(key, 128, &aes_key);
AES_encrypt(input, output, &aes_key);
// 性能对比:
// VM 版本:~50ms
// 标准版本:~0.5ms (100x 加速!)
}
BOOL WINAPI DllMain(HINSTANCE hinstDLL, DWORD fdwReason, LPVOID lpvReserved) {
if (fdwReason == DLL_PROCESS_ATTACH) {
// Hook VM 入口点
DWORD oldProtect;
void* vm_entry = (void*)0x5A3000;
VirtualProtect(vm_entry, 5, PAGE_EXECUTE_READWRITE, &oldProtect);
// 写入跳转到我们的函数
unsigned char jmp_code[] = {
0xE9, 0x00, 0x00, 0x00, 0x00 // jmp rel32
};
DWORD offset = (DWORD)VM_AES_Encrypt - (DWORD)vm_entry - 5;
memcpy(&jmp_code[1], &offset, 4);
memcpy(vm_entry, jmp_code, 5);
VirtualProtect(vm_entry, 5, oldProtect, &oldProtect);
}
return TRUE;
}
案例三:商业软件保护分析(专家级)
📋 目标信息
text
程序:ProCAD_2023.exe
保护:VMProtect 3.6.0 Ultimate (最高级别)
特点:多层 VM + 硬件指纹 + 网络验证
目标:分析许可证验证机制
难度:⭐⭐⭐⭐⭐
🚧 超高难度特征
asm
; VM 嵌套示例
Layer 1 VM:
call VM_L1_Entry
; 内部调用 Layer 2
Layer 2 VM:
call VM_L2_Entry
; 内部调用 Layer 3
Layer 3 VM:
; 实际验证逻辑(深埋三层)
call CheckHWID_VM
call VerifyLicense_VM
call ContactServer_VM
🎯 实战策略:多管齐下
策略 1:API 监控(绕过 VM)
Python
# 使用 Frida Hook 关键 API
import frida
import sys
js_code = """
// Hook 网络函数
Interceptor.attach(Module.findExportByName("ws2_32.dll", "send"), {
onEnter: function(args) {
var buf = Memory.readByteArray(args[1], args[2].toInt32());
console.log("Send data:", hexdump(buf));
// 发现验证请求格式
// POST /api/verify
// {"hwid": "...", "license": "..."}
}
});
// Hook 注册表读取
Interceptor.attach(Module.findExportByName("advapi32.dll", "RegQueryValueExW"), {
onEnter: function(args) {
var keyName = Memory.readUtf16String(args[1]);
console.log("Read registry:", keyName);
// 发现许可证存储位置
// HKLM\\Software\\ProCAD\\License
}
});
// Hook 文件操作
Interceptor.attach(Module.findExportByName("kernel32.dll", "CreateFileW"), {
onEnter: function(args) {
var filename = Memory.readUtf16String(args[0]);
if (filename.includes(".lic")) {
console.log("License file:", filename);
this.isLicense = true;
}
},
onLeave: function(retval) {
if (this.isLicense) {
console.log("License handle:", retval);
}
}
});
"""
# 执行监控
session = frida.attach("ProCAD_2023.exe")
script = session.create_script(js_code)
script.load()
sys.stdin.read()
监控结果
text
[+] Send data:
POST /api/verify HTTP/1.1
Content-Type: application/json
{"hwid":"E3B0C442...","license":"PCAD-2023-XXXX-XXXX"}
[+] Read registry: LicenseKey
[+] License file: C:\ProgramData\ProCAD\license.lic
策略 2:差分分析
Python
# 使用合法试用版 vs 完整版对比
# 1. 运行试用版,dump 内存
# 2. 运行完整版,dump 内存
# 3. 对比差异
import difflib
trial_memory = open("trial_dump.bin", "rb").read()
full_memory = open("full_dump.bin", "rb").read()
# 查找差异
for i in range(0, min(len(trial_memory), len(full_memory)), 4):
trial_val = int.from_bytes(trial_memory[i:i+4], 'little')
full_val = int.from_bytes(full_memory[i:i+4], 'little')
if trial_val != full_val:
print(f"Diff at 0x{i:08X}: {trial_val:08X} -> {full_val:08X}")
# 输出:
# Diff at 0x005A7F20: 00000000 -> 00000001 ← 许可证标志!
# Diff at 0x005A7F24: 0000000E -> FFFFFFFF ← 试用天数
策略 3:时间旅行调试(Time-Travel Debugging)
Bash
# 使用 Windows TTD (WinDbg Preview)
# 1. 录制执行过程
ttd.exe -out trace.run ProCAD_2023.exe
# 2. 分析录制文件
windbgx -z trace.run
# 3. 在 WinDbg 中查找关键点
0:000> .tassert 0x5A7F20 == 1 # 找到许可证标志变为 1 的时刻
Time Travel Position: 12A4F:3B
0:000> !tt 12A4F:3B - 1000 # 回退 1000 步
0:000> k # 查看调用栈
# 发现调用链:
VM_Layer3_Handler_0xA7
→ DecryptLicense
→ ParseLicenseData
→ SetLicenseFlag ← 就是这里!
📊 综合分析成果
发现的验证流程
text
用户输入许可证
↓
[VM Layer 1] 解密许可证文件
↓
[VM Layer 2] 提取硬件指纹
↓
[VM Layer 3] 计算校验值
↓
网络验证(可选)
↓
[VM Layer 2] 检查有效期
↓
[VM Layer 1] 设置授权标志
↓
返回验证结果
提取的关键数据
Python
# 许可证格式(逆向得到)
class ProCADLicense:
def __init__(self, license_string):
# PCAD-2023-A1B2-C3D4-E5F6
parts = license_string.split('-')
self.product = parts[0] # PCAD
self.version = parts[1] # 2023
self.user_id = parts[2] # A1B2
self.checksum = parts[3] # C3D4
self.flags = parts[4] # E5F6
def validate(self, hwid):
# 从 VM 中还原的算法
data = f"{self.user_id}{hwid}{self.version}".encode()
# 关键发现:使用 MD5(从 VM 内存中识别)
import hashlib
expected = hashlib.md5(data).hexdigest()[:4].upper()
return expected == self.checksum
# 测试
lic = ProCADLicense("PCAD-2023-A1B2-C3D4-E5F6")
hwid = get_hwid() # 从 Frida 监控获得的函数
if lic.validate(hwid):
print("[+] License valid!")
🏆 最终解决方案
方案 A:内存 Patch(临时)
Python
# 使用 Cheat Engine 或脚本
import pymem
pm = pymem.Pymem("ProCAD_2023.exe")
# 找到许可证检查标志(通过差分分析)
license_flag_addr = pm.base_address + 0x5A7F20
# 写入合法标志
pm.write_int(license_flag_addr, 1)
# 写入永久有效期
expiry_addr = pm.base_address + 0x5A7F24
pm.write_int(expiry_addr, 0xFFFFFFFF)
print("[+] License activated in memory")
方案 B:loader(永久)
C++
// ProCAD_Loader.exe
#include <windows.h>
int main() {
// 1. 创建挂起进程
STARTUPINFO si = {0};
PROCESS_INFORMATION pi = {0};
CreateProcess("ProCAD_2023.exe", NULL, NULL, NULL, FALSE,
CREATE_SUSPENDED, NULL, NULL, &si, &pi);
// 2. 注入 Patch
LPVOID addr = VirtualAllocEx(pi.hProcess, NULL, 4096,
MEM_COMMIT, PAGE_EXECUTE_READWRITE);
// Shellcode: 跳过许可证检查
unsigned char patch[] = {
0xB8, 0x01, 0x00, 0x00, 0x00, // mov eax, 1
0xC3 // ret
};
// 写入目标地址(VM 出口处)
DWORD_PTR target = (DWORD_PTR)GetModuleHandle(NULL) + 0x5A9F20;
WriteProcessMemory(pi.hProcess, (LPVOID)target, patch, sizeof(patch), NULL);
// 3. 恢复执行
ResumeThread(pi.hThread);
return 0;
}
📚 工具汇总表
| 工具 | 用途 | 案例 | 下载 |
|---|---|---|---|
| x64dbg | 动态调试 | 所有 | https://x64dbg.com |
| IDA Pro | 静态分析 | 所有 | 商业软件 |
| VMPTrace | VM 跟踪 | 案例1、2 | GitHub |
| Triton | 符号执行 | 案例2 | GitHub |
| Frida | API Hook | 案例3 | https://frida.re |
| Scylla | IAT 修复 | 脱壳后 | GitHub |
| TTD | 时间旅行 | 案例3 | Windows SDK |
⚠️ 难度与时间评估
text
案例一(简单VM)
├─ 准备工作:2-4 小时
├─ 动态跟踪:4-8 小时
├─ 还原代码:8-16 小时
└─ 总计:1-3 天
案例二(复杂算法)
├─ 环境配置:4-8 小时
├─ 符号执行:16-40 小时
├─ 算法识别:20-60 小时
└─ 总计:2-4 周
案例三(商业级)
├─ 多工具联合:40+ 小时
├─ 深度分析:80-200 小时
├─ 反复验证:40-80 小时
└─ 总计:1-3 个月
🎓 学习路线建议
入门阶段
- 掌握 x86/x64 汇编
- 熟悉 IDA Pro 基础操作
- 练习简单加壳程序(UPX、ASPack)
进阶阶段
- 学习 VMProtect 基础原理
- 分析开源 VM 项目(Tigress、OLLVM)
- 尝试案例一级别的脱壳
高级阶段
- 掌握符号执行框架(Triton/Angr)
- 学习二进制插桩(Pin/DynamoRIO)
- 挑战案例二、三级别
💡 关键技巧总结
- 不要试图完全还原 - 只需理解关键逻辑
- 利用侧信道 - API 监控、内存差分
- 自动化工具优先 - 手工分析做补充
- 验证每一步 - 用已知输入测试中间结果
- 记录详细笔记 - VM 分析极易迷失方向
需要更深入的技术细节吗?我可以讲解:
- Handler 分析技巧
- 虚拟机栈回溯方法
- 实际脱 VM 案例
浙公网安备 33010602011771号