使用 Hyperpwn 调试 VMP 并构建映射表

前置准备

在上一篇文章中,我们完成了 Hyperpwn 的安装和基本配置。本文将通过一个实际案例,演示如何使用 Hyperpwn 调试 VMP 保护下的函数,并构建完整的 Handler 映射表。

环境准备

  • IDA Pro 7.5+(已安装 Hyperpwn 插件)
  • 目标 SO 文件:包含 VMP 保护的 Native 函数
  • Android 模拟器或真机(用于动态调试)
  • 已知的被保护函数入口地址

连接调试目标

使用 IDA 远程调试

Hyperpwn 的 VM Trace 功能需要配合 IDA 的调试器使用。以下是连接步骤:

1. 在 Android 设备上启动 Android Server

adb push android_server /data/local/tmp/
adb shell chmod 755 /data/local/tmp/android_server
adb shell /data/local/tmp/android_server

2. IDA 中配置远程调试

Debugger → Select debugger → Remote ARM Linux/Android debugger
Debugger → Process options → Hostname: <设备IP>, Port: 23946

3. 附加到目标进程

Debugger → Attach to process → 选择目标 APP 的进程

4. 确认基址

附加成功后,在 IDA 中按 Ctrl+S 查看模块列表,确认目标 SO 文件的加载基址:

Module         Base           End            Size
libnative.so   0x7F00000000   0x7F00010000   0x10000

记录基址,后续所有地址需要加上此基址偏移。

设置 VM Trace 追踪执行流

启动 Hyperpwn VM Trace

在 IDA 中加载 Hyperpwn 后:

Hyperpwn → VM Trace → Configure Trace

配置参数:

VM Entry Point:  <被保护函数地址 + 基址偏移>
Trace Mode:      Full Trace (记录所有执行信息)
Max Instructions: 50000
Output File:     vm_trace_log.txt

设置断点

在 VM 的分发器入口处设置断点。Hyperpwn 会尝试自动定位分发器,你也可以手动指定:

; 典型的 ARM 分发器入口
LDRB    R0, [R1], #1          ; 从字节码读取操作码
LDR     R2, =handler_table    ; 加载 Handler 表基址
LDR     R3, [R2, R0, LSL #2]  ; 查表获取 Handler 地址
BX      R3                    ; 跳转到 Handler

Hyperpwn 在分发器处设置条件日志断点,每次执行到分发器时记录:

  • 当前 vPC(虚拟程序计数器)的值
  • 读取的操作码
  • 跳转到的 Handler 地址
  • 当前虚拟寄存器的状态

执行追踪

配置完成后,让目标函数执行:

方法一:在 APP 中触发对应功能
方法二:使用 Frida 脚本调用目标函数
方法三:在 IDA 中使用 Debugger → Run

追踪过程中,IDA 的输出窗口会实时显示执行日志。

提取所有 VM Handler

自动提取

VM Trace 运行一段时间后(建议覆盖函数的主要逻辑路径),停止追踪并提取 Handler:

Hyperpwn → Handler → Extract from Trace

Hyperpwn 会分析 Trace 日志,提取所有被访问过的 Handler 地址,并尝试识别其语义:

[*] Analyzing trace log...
[*] VM Dispatcher identified at 0x7F00001A00
[*] Handler table base: 0x7F00004000
[*] Extracting handlers...

  #  OP   Address          Size   Category      Auto-Semantic
  ──────────────────────────────────────────────────────────────
  1  0x01  0x7F00001A20     16     Arithmetic   疑似 ADD
  2  0x02  0x7F00001A30     16     Arithmetic   疑似 SUB
  3  0x03  0x7F00001A40     16     Arithmetic   疑似 XOR
  4  0x04  0x7F00001A50     12     Data Transfer 疑似 MOV
  5  0x05  0x7F00001A5C     14     Data Transfer 疑似 LOAD_IMM
  6  0x06  0x7F00001A6A     14     Memory        疑似 STORE
  7  0x07  0x7F00001A78     14     Memory        疑似 LOAD
  8  0x08  0x7F00001A86     10     Compare       疑似 CMP
  9  0x09  0x7F00001A90     18     Branch        疑似 JZ
  10 0x0A  0x7F00001AA2     18     Branch        疑似 JNZ
  11 0x0B  0x7F00001AB4     12     Call          疑似 CALL
  12 0x0C  0x7F00001AC0     8      Call          疑似 RET
  ...

补充未覆盖的 Handler

Trace 只能覆盖实际执行到的 Handler。为了获得完整的 Handler 列表,需要补充扫描:

Hyperpwn → Handler → Scan All (Full Table)

Hyperpwn 会扫描 Handler 跳转表中的所有条目,即使它们在 Trace 中没有被执行到:

[*] Full table scan...
[*] Handler table has 256 entries
[*] 47 valid handlers found (209 entries are NULL or invalid)
[*] 35 handlers not covered by trace - will need manual analysis

手动分析未覆盖的 Handler

对于 Trace 未覆盖的 Handler,需要在 IDA 中手动查看:

跳转到 Handler 地址 → 查看反汇编代码 → 判断语义

例如,分析一个未识别的 Handler:

; Handler @ 0x7F00001B00
LDRB    R0, [R1], #1          ; 读取寄存器编号
LDR     R2, [vRegs, R0, LSL #2]; 加载虚拟寄存器值
MVN     R2, R2                ; 按位取反(NOT)
LDRB    R3, [R1], #1          ; 读取目的寄存器编号
STR     R2, [vRegs, R3, LSL #2]; 存回结果
B       dispatcher            ; 返回分发器

结论:这是一个 NOT(按位取反)Handler

构建 Handler 映射表

映射表的结构

Handler 映射表是整个 VMP 逆向分析的核心成果。一个完整的映射表应该包含以下字段:

┌────────┬──────────────┬──────┬──────────┬──────────┬─────────────────┐
│ Opcode │ Handler Addr │ Size │ Category │ Semantic │ Operand Format  │
├────────┼──────────────┼──────┼──────────┼──────────┼─────────────────┤
│ 0x01   │ 0x7F00001A20 │ 16   │ Arith    │ ADD      │ vDst,vS1,vS2   │
│ 0x02   │ 0x7F00001A30 │ 16   │ Arith    │ SUB      │ vDst,vS1,vS2   │
│ 0x03   │ 0x7F00001A40 │ 16   │ Arith    │ XOR      │ vDst,vS1,vS2   │
│ 0x04   │ 0x7F00001A50 │ 12   │ Transfer │ MOV      │ vDst,vSrc      │
│ 0x05   │ 0x7F00001A5C │ 14   │ Transfer │ LOAD_IMM │ vDst,imm32     │
│ 0x06   │ 0x7F00001A6A │ 14   │ Memory   │ STORE    │ vSrc,addr32    │
│ 0x07   │ 0x7F00001A78 │ 14   │ Memory   │ LOAD     │ vDst,addr32    │
│ 0x08   │ 0x7F00001A86 │ 10   │ Compare  │ CMP      │ vS1,vS2        │
│ 0x09   │ 0x7F00001A90 │ 18   │ Branch   │ JZ       │ offset32       │
│ 0x0A   │ 0x7F00001AA2 │ 18   │ Branch   │ JNZ      │ offset32       │
│ 0x0B   │ 0x7F00001AB4 │ 12   │ Call     │ CALL     │ target_addr    │
│ 0x0C   │ 0x7F00001AC0 │ 8    │ Call     │ RET      │ (none)         │
│ 0x0D   │ 0x7F00001AC8 │ 14   │ Shift    │ SHL      │ vDst,vS1,vS2   │
│ 0x0E   │ 0x7F00001AD6 │ 14   │ Shift    │ SHR      │ vDst,vS1,vS2   │
│ 0x0F   │ 0x7F00001AE4 │ 14   │ Logic    │ AND      │ vDst,vS1,vS2   │
│ 0x10   │ 0x7F00001AF2 │ 14   │ Logic    │ OR       │ vDst,vS1,vS2   │
│ 0x11   │ 0x7F00001B00 │ 12   │ Logic    │ NOT      │ vDst,vSrc      │
│ 0x12   │ 0x7F00001B0C │ 10   │ Stack    │ PUSH     │ vSrc           │
│ 0x13   │ 0x7F00001B16 │ 10   │ Stack    │ POP      │ vDst           │
└────────┴──────────────┴──────┴──────────┴──────────┴─────────────────┘

使用 Hyperpwn 导出映射表

完成 Handler 分析后,通过 Hyperpwn 导出映射表:

Hyperpwn → Export → Handler Map → handler_map.json

导出的 JSON 格式:

{
    "vm_info": {
        "dispatcher_addr": "0x7F00001A00",
        "handler_table_base": "0x7F00004000",
        "handler_count": 47,
        "virtual_reg_count": 16,
        "architecture": "ARM32"
    },
    "handlers": [
        {
            "opcode": "0x01",
            "address": "0x7F00001A20",
            "size": 16,
            "category": "arithmetic",
            "semantic": "ADD",
            "operand_format": "vDst,vS1,vS2",
            "description": "vDst = vS1 + vS2"
        },
        ...
    ]
}

在 IDA 中标记 Handler

Hyperpwn 还会在 IDA 中自动为每个 Handler 添加注释:

; ═══════════════════════════════════════
; VM Handler: OP_XOR (0x03)
; Category: Arithmetic
; Operand Format: vDst, vSrc1, vSrc2
; Semantic: vDst = vSrc1 ^ vSrc2
; Size: 16 bytes
; ═══════════════════════════════════════
handler_xor:
    LDRB    R0, [R1], #1
    LDRB    R2, [R1], #1
    LDRB    R3, [R1], #1
    LDR     R4, [vRegs, R2, LSL #2]
    LDR     R5, [vRegs, R3, LSL #2]
    EOR     R4, R4, R5
    STR     R4, [vRegs, R0, LSL #2]
    B       dispatcher

分析 VM 调度器的跳转逻辑

理解调度器如何工作,是构建正确映射表的关键。

跳转表的结构

在 IDA 中查看 Handler 跳转表:

.data:7F00004000  DCD handler_nop      ; OP 0x00
.data:7F00004004  DCD handler_add      ; OP 0x01
.data:7F00004008  DCD handler_sub      ; OP 0x02
.data:7F0000400C  DCD handler_xor      ; OP 0x03
.data:7F00004010  DCD handler_mov      ; OP 0x04
.data:7F00004014  DCD handler_load_imm ; OP 0x05
...

每个条目是一个 4 字节的地址(ARM32),按操作码顺序排列。分发器使用 操作码 × 4 作为偏移量来索引。

调度器的优化

一些高级 VMP 的调度器会使用更复杂的跳转方式:

; 加密跳转表:地址经过异或处理
LDR     R3, [R2, R0, LSL #2]  ; 读取加密后的地址
EOR     R3, R3, #0xDEADBEEF   ; 解密
BX      R3                    ; 跳转

; 多级跳转:通过中间层间接跳转
LDR     R3, [R2, R0, LSL #2]  ; 第一级表
LDR     R3, [R3]              ; 第二级间接寻址
BX      R3

Hyperpwn 会尝试自动检测这些模式,如果自动检测失败,你需要手动指定调度器的跳转逻辑。

从字节码序列还原原始指令

有了完整的映射表,就可以将字节码序列还原为可读的操作序列。

手动还原示例

假设 Trace 捕获到以下字节码序列(Hex):

05 00 41 42 43 44    →  OP 0x05: vReg[0] = 0x44434241
05 01 EF BE AD DE    →  OP 0x05: vReg[1] = 0xDEADBEEF
03 02 00 01          →  OP 0x03: vReg[2] = vReg[0] ^ vReg[1]
04 03 02             →  OP 0x04: vReg[3] = vReg[2]
09 10 00 00 00       →  OP 0x09: if (ZF) jump +0x10

对照映射表还原:

// 还原后的伪 C 代码
vReg[0] = 0x44434241;       // 'ABCD'
vReg[1] = 0xDEADBEEF;
vReg[2] = vReg[0] ^ vReg[1]; // XOR 运算
vReg[3] = vReg[2];           // 移动结果
if (zero_flag) goto offset;  // 条件跳转

使用 Hyperpwn 自动反编译

Hyperpwn → Bytecode → Decompile from Trace

Hyperpwn 会自动读取 Trace 日志中的字节码序列,对照映射表进行反编译,输出更可读的结果:

// Hyperpwn 反编译输出
// Function: sub_7F00005000 (VMP Protected)
// Decompiled from VM Bytecode

uint32_t v0, v1, v2, v3;

v0 = 0x44434241;               // [bytecode +0x00] LOAD_IMM
v1 = 0xDEADBEEF;               // [bytecode +0x06] LOAD_IMM
v2 = v0 ^ v1;                  // [bytecode +0x0C] XOR
v3 = v2;                       // [bytecode +0x10] MOV
if (flags.ZF) goto BB_0x22;    // [bytecode +0x13] JZ

// Basic Block 2
v2 = v2 << 8;                  // [bytecode +0x18] SHL
v3 = v3 + 1;                   // [bytecode +0x1E] ADD
// fall through

// Basic Block 3 (target of BB_0x22)
*(uint32_t*)0x7FFF0010 = v3;   // [bytecode +0x22] STORE
return v2;                     // [bytecode +0x28] RET

映射表在自动还原中的应用

构建好映射表后,它可以被用于自动化分析工具,实现 VMP 代码的批量还原。

集成到 IDA 脚本

# 使用映射表自动标注字节码的 IDA 脚本

import json

# 加载映射表
with open('handler_map.json', 'r') as f:
    handler_map = json.load(f)

# 构建操作码到语义的快速查找表
opcode_lookup = {}
for h in handler_map['handlers']:
    opcode_lookup[int(h['opcode'], 16)] = h

# 遍历字节码段,添加注释
bytecode_start = 0x7F00006000
bytecode_end = 0x7F00007000
pc = bytecode_start

while pc < bytecode_end:
    op = idc.read_unsigned_byte(pc)
    if op in opcode_lookup:
        h = opcode_lookup[op]
        comment = f"VM_{h['semantic']}({h['operand_format']})"
        idc.set_cmt(pc, comment, 0)
        pc += h['size']  # 跳过操作数
    else:
        pc += 1

print("Bytecode annotation complete!")

批量还原多个函数

如果一个 SO 文件中有多个函数被同一 VMP 保护,映射表可以被复用:

# 对多个 VMP 函数使用同一映射表

vmp_functions = [
    0x7F00005000,  # 函数1
    0x7F00005500,  # 函数2
    0x7F00005A00,  # 函数3
]

for func_addr in vmp_functions:
    print(f"\n=== Analyzing function at 0x{func_addr:X} ===")
    decompile_vm_function(func_addr, handler_map)

实际案例:还原 VMP 保护下的加密函数

背景信息

目标:某金融 APP 中的 encrypt_data 函数,负责加密用户数据。

已知信息:

  • 函数入口:0x7F00005000
  • 参数:char* data, char* key, int len
  • 返回值:加密后的数据指针

分析步骤

第一步:运行 VM Trace

使用 Hyperpwn 追踪函数执行,获取完整的 Trace 日志。

第二步:提取 Handler 映射表

根据 Trace 结果,构建了 47 个 Handler 的完整映射表。

第三步:反编译字节码

使用映射表反编译 Trace 中的字节码序列,得到以下伪代码:

// 反编译结果
uint32_t encrypt_data(char* data, char* key, int len) {
    uint8_t sbox[256];      // vStack 对应的 VM 内存区域
    uint32_t i, j, t;
    
    // S-box 初始化(KSA)
    for (i = 0; i < 256; i++) {
        sbox[i] = i;                        // [bytecode +0x00 ~ +0x3C]
    }
    
    j = 0;
    for (i = 0; i < 256; i++) {
        j = (j + sbox[i] + key[i % len]) & 0xFF;  // [bytecode +0x3D ~ +0x80]
        // swap sbox[i], sbox[j]
        t = sbox[i];
        sbox[i] = sbox[j];
        sbox[j] = t;
    }
    
    // PRGA 加密
    i = 0; j = 0;
    for (int k = 0; k < len; k++) {
        i = (i + 1) & 0xFF;
        j = (j + sbox[i]) & 0xFF;
        t = sbox[i];
        sbox[i] = sbox[j];
        sbox[j] = t;
        data[k] ^= sbox[(sbox[i] + sbox[j]) & 0xFF];
    }
    
    return data;
}

第四步:识别算法

通过反编译结果可以清晰识别:这是一个标准的 RC4 加密算法(KSA + PRGA 结构)。VMP 虽然虚拟化了代码,但算法的宏观结构(256 次循环初始化 S-box、PRGA 流加密)仍然保留。

总结

使用 Hyperpwn 调试 VMP 并构建映射表是系统化的 VMP 逆向流程。核心步骤是:VM Trace 追踪执行流 → 提取 Handler → 构建映射表 → 反编译字节码 → 识别算法逻辑。掌握了这个流程,面对 VMP 保护下的函数就不再是"黑盒",而是可以逐步拆解的"灰盒"。