GKLBB

当你经历了暴风雨,你也就成为了暴风雨

导航

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 但看起来像:

  1. 编译器优化过度
C
// -O3 优化 + 内联展开
可能产生复杂代码,但:
✓ F5 仍能反编译
✓ 有明确的函数调用
  1. 手写汇编优化
asm
; 加密算法常用查表
movzx eax, byte ptr [esi]
mov   al, [sbox + eax]  ; ← 不是 VM
  1. 控制流平坦化(单独使用)
text
没有 VM 的完整特征:
- 缺少 VM Context
- 缺少 Handler 碎片
- F5 可以反编译(虽然难看)

🎓 最终建议

判断优先级(重要性排序):

  1. F5 无法反编译 → 最强信号
  2. 存在 Handler 跳转表 → 确认 VM
  3. 平坦化控制流 → 高度怀疑
  4. PUSHAD 入口模式 → 强烈怀疑
  5. 超大函数 (>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 个月

🎓 学习路线建议

入门阶段

  1. 掌握 x86/x64 汇编
  2. 熟悉 IDA Pro 基础操作
  3. 练习简单加壳程序(UPX、ASPack)

进阶阶段

  1. 学习 VMProtect 基础原理
  2. 分析开源 VM 项目(Tigress、OLLVM)
  3. 尝试案例一级别的脱壳

高级阶段

  1. 掌握符号执行框架(Triton/Angr)
  2. 学习二进制插桩(Pin/DynamoRIO)
  3. 挑战案例二、三级别

💡 关键技巧总结

  1. 不要试图完全还原 - 只需理解关键逻辑
  2. 利用侧信道 - API 监控、内存差分
  3. 自动化工具优先 - 手工分析做补充
  4. 验证每一步 - 用已知输入测试中间结果
  5. 记录详细笔记 - VM 分析极易迷失方向

 

 

需要更深入的技术细节吗?我可以讲解:

 

  1. Handler 分析技巧
  2. 虚拟机栈回溯方法
  3. 实际脱 VM 案例

posted on 2025-10-27 11:28  GKLBB  阅读(179)  评论(0)    收藏  举报