使用 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 保护下的函数就不再是"黑盒",而是可以逐步拆解的"灰盒"。