关于 vm-and-bytecode-reverse
用于分析自定义虚拟机、字节码解释器和 VM 保护二进制文件的逆向工程技能。内容介绍调度器识别、操作码映射、反汇编器编写,以及 CTF 迷宫题和实际保护器的分析方法。
主要功能
- 01识别常见 VM 调度器模式
- 02映射操作码、操作数和副作用
- 03指导提取字节码并编写反汇编器
- 04涵盖基于栈和寄存器的 VM
- 05提供迷宫求解与保护器分析流程
适用场景
- 01逆向分析自定义字节码解释器
- 02分析基于 VM 的 CTF 挑战
- 03调查受 VM 保护的二进制文件
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ yaklang/ hack-skills --skill vm-and-bytecode-reverse安装后在 Claude Code 中输入/vm-and-bytecode-reverse确认已加载。
详细介绍
技能:虚拟机与字节码逆向工程 — 专家分析手册
AI 加载说明:逆向分析自定义虚拟机和字节码解释器的专家技巧。涵盖分发器识别、操作码映射、自定义 ISA 重构、反汇编器/反编译器编写、迷宫挑战以及真实世界中的虚拟机保护器分析。基础模型往往无法识别取指-解码-执行模式,或试图将虚拟机字节码作为原生代码分析。
0. 相关路由
- 当虚拟机属于商业保护器(VMProtect/Themida)时,使用 code-obfuscation-deobfuscation
- 使用 angr 求解基于虚拟机的挑战时,使用 symbolic-execution-tools
- 当虚拟机包含反调试检查时,使用 anti-debugging-techniques
快速识别
| 二进制特征 | 可能的虚拟机类型 | 从何处入手 |
|---|---|---|
while(1) { switch(bytecode[pc]) } | 基于 switch 的分发器 | 将每个分支映射到一个操作 |
通过表进行间接跳转 jmp [table + opcode*8] | 基于表的分发器 | 转储跳转表并分析处理程序 |
| 根据字节值构成的嵌套 if-else 链 | 基于 if 链的分发器 | 与 switch 相同,只是语法不同 |
| 以栈推入/弹出操作为主 | 基于栈的虚拟机 | 识别推入、弹出和算术操作 |
reg[X] = ... 数组操作 | 基于寄存器的虚拟机 | 将寄存器索引映射到操作 |
| 二维网格 + 方向输入 | 迷宫挑战 | 提取网格并应用 BFS/DFS |
1. 自定义虚拟机识别
1.1 结构特征
VM Architecture Components:
┌─────────────────────────────────┐
│ Bytecode Program (data section)│
├─────────────────────────────────┤
│ Program Counter (pc/ip) │
│ Register File / Stack │
│ Memory / Data Area │
├─────────────────────────────────┤
│ Dispatcher Loop │
│ ├─ Fetch: opcode = code[pc] │
│ ├─ Decode: lookup handler │
│ └─ Execute: run handler │
└─────────────────────────────────┘
1.2 IDA/Ghidra 特征
Switch 分发器(CTF 中最常见):
while (running) {
unsigned char op = bytecode[pc++];
switch (op) {
case 0x00: /* nop */ break;
case 0x01: /* push imm */ stack[sp++] = bytecode[pc++]; break;
case 0x02: /* add */ stack[sp-2] += stack[sp-1]; sp--; break;
// ...
case 0xFF: /* halt */ running = 0; break;
}
}
表分发器(优化程度更高):
typedef void (*handler_t)(vm_ctx_t*);
handler_t handlers[256] = { handle_nop, handle_push, handle_add, ... };
while (running) {
handlers[bytecode[pc++]](&ctx);
}
2. 分析方法
步骤 1:找到分发器
查找:
- 循环中的大型 switch 语句(包含许多分支)
- 由数据缓冲区中的字节索引的函数指针数组
- 圈复杂度很高的单个函数
- 指向按字节读取的数据缓冲区的交叉引用
步骤 2:将操作码映射到操作
对于每个分支/处理程序,确定:
| 属性 | 识别方法 |
|---|---|
| 操作码值 | 分支编号或表索引 |
| 操作类型 | 寄存器/栈的修改 |
| 操作数数量 | 操作码后消耗的字节数 |
| 操作数类型 | 立即数、寄存器索引或内存地址 |
| 副作用 | 输出、内存写入、标志修改 |
步骤 3:提取字节码程序
# Typical extraction from binary
import struct
with open('challenge', 'rb') as f:
f.seek(bytecode_offset)
bytecode = f.read(bytecode_length)
# Or from IDA:
# bytecode = idc.get_bytes(bytecode_addr, bytecode_len)
步骤 4:编写自定义反汇编器
OPCODES = {
0x00: ("nop", 0), # (mnemonic, operand_bytes)
0x01: ("push", 1), # push immediate byte
0x02: ("pop", 0),
0x03: ("add", 0),
0x04: ("sub", 0),
0x05: ("xor", 0),
0x06: ("cmp", 0),
0x07: ("jmp", 2), # jump to 16-bit address
0x08: ("je", 2),
0x09: ("jne", 2),
0x0A: ("mov", 2), # mov reg, imm
0x0B: ("load", 1), # load from memory[operand]
0x0C: ("store",1), # store to memory[operand]
0x0D: ("print",0),
0x0E: ("read", 0), # read input
0xFF: ("halt", 0),
}
def disassemble(bytecode):
pc = 0
while pc < len(bytecode):
op = bytecode[pc]
if op not in OPCODES:
print(f" {pc:04x}: UNKNOWN {op:#04x}")
pc += 1
continue
mnemonic, operand_size = OPCODES[op]
operands = bytecode[pc+1:pc+1+operand_size]
operand_str = ' '.join(f'{b:#04x}' for b in operands)
print(f" {pc:04x}: {mnemonic:8s} {operand_str}")
pc += 1 + operand_size
disassemble(bytecode)
步骤 5:分析反汇编后的程序
使用自定义反汇编结果,应用标准逆向工程方法:
- 识别输入读取操作(读取操作码)
- 跟踪从输入到比较操作的数据流
- 确定成功/失败条件
- 提取检查逻辑(通常会对输入执行 XOR/ADD 转换,再与常量比较)
3. CTF 中常见的虚拟机模式
3.1 基于栈的虚拟机
操作通过栈执行(类似 JVM 或 Python 字节码)。
| 操作码 | 操作 | 栈效果 |
|---|---|---|
| PUSH imm | 推入立即数 | [...] → [..., imm] |
| POP | 丢弃栈顶元素 | [..., a] → [...] |
| ADD | 将栈顶两个值相加 | [..., a, b] → [..., a+b] |
| SUB | 相减 | [..., a, b] → [..., a-b] |
| MUL | 相乘 | [..., a, b] → [..., a*b] |
| XOR | 按位异或 | [..., a, b] → [..., a^b] |
| CMP | 比较 | [..., a, b] → [..., (a==b)] |
| JMP addr | 无条件跳转 | 不变 |
| JZ addr | 栈顶元素为零时跳转 | [..., a] → [...] |
| 将栈顶元素作为字符输出 | [..., a] → [...] | |
| READ | 读取字符并压入栈 | [...] → [..., input] |
| HALT | 停止执行 | - |
3.2 基于寄存器的 VM
操作使用寄存器索引(类似 x86、ARM)。
| 操作码 | 格式 | 操作 |
|---|---|---|
| MOV r, imm | 0x01 RR II II | reg[R] = imm16 |
| MOV r1, r2 | 0x02 R1 R2 | reg[R1] = reg[R2] |
| ADD r1, r2 | 0x03 R1 R2 | reg[R1] += reg[R2] |
| SUB r1, r2 | 0x04 R1 R2 | reg[R1] -= reg[R2] |
| XOR r1, r2 | 0x05 R1 R2 | reg[R1] ^= reg[R2] |
| CMP r1, r2 | 0x06 R1 R2 | flags = compare(r1, r2) |
| JMP addr | 0x07 AA AA | pc = addr |
| JE addr | 0x08 AA AA | if equal: pc = addr |
| LOAD r, [addr] | 0x09 RR AA | reg[R] = mem[addr] |
| STORE [addr], r | 0x0A AA RR | mem[addr] = reg[R] |
| SYSCALL | 0x0B | 根据 reg[0] 执行 I/O 操作 |
| HALT | 0xFF | 停止 |
3.3 类 Brainfuck/Esoteric VM
| BF 命令 | VM 等效操作 | 描述 |
|---|---|---|
> | INC ptr | 数据指针右移 |
< | DEC ptr | 数据指针左移 |
+ | INC [ptr] | 将指针所指位置的字节递增 |
- | DEC [ptr] | 将指针所指位置的字节递减 |
. | OUTPUT [ptr] | 输出指针所指位置的字节 |
, | INPUT [ptr] | 向指针所指位置输入字节 |
[ | JZ forward | 如果字节为零,则跳过 ] |
] | JNZ back | 如果字节非零,则跳回 [ |
4. 迷宫挑战
4.1 识别
- 二进制程序读取方向输入(WASD、箭头键、UDLR)
- 数据段中有二维数组(墙壁、路径、起点、终点)
- 使用 x、y 坐标跟踪位置
- 在特定坐标处满足胜利条件
4.2 提取地图
# Extract maze grid from binary data section
MAZE_ADDR = 0x601060
WIDTH = 20
HEIGHT = 15
# From binary dump:
maze = []
for row in range(HEIGHT):
line = ""
for col in range(WIDTH):
cell = bytecode[MAZE_ADDR + row * WIDTH + col - base_addr]
if cell == 0: line += "." # path
elif cell == 1: line += "#" # wall
elif cell == 2: line += "S" # start
elif cell == 3: line += "E" # end
else: line += "?"
maze.append(line)
print(line)
4.3 自动求解
from collections import deque
def solve_maze(maze, start, end):
"""BFS solver returns direction string."""
rows, cols = len(maze), len(maze[0])
directions = {'U': (-1, 0), 'D': (1, 0), 'L': (0, -1), 'R': (0, 1)}
queue = deque([(start, "")])
visited = {start}
while queue:
(r, c), path = queue.popleft()
if (r, c) == end:
return path
for name, (dr, dc) in directions.items():
nr, nc = r + dr, c + dc
if (0 <= nr < rows and 0 <= nc < cols and
maze[nr][nc] != '#' and (nr, nc) not in visited):
visited.add((nr, nc))
queue.append(((nr, nc), path + name))
return None
# Find start and end positions
for r, row in enumerate(maze):
for c, cell in enumerate(row):
if cell == 'S': start = (r, c)
if cell == 'E': end = (r, c)
solution = solve_maze(maze, start, end)
print(f"Path: {solution}")
4.4 方向编码
不同挑战对方向的编码方式各不相同:
| 编码 | 上 | 下 | 左 | 右 |
|---|---|---|---|---|
| WASD | W | S | A | D |
| UDLR | U | D | L | R |
| 箭头键 | ↑ (0x48) | ↓ (0x50) | ← (0x4B) | → (0x4D) |
| 数字 | 1 | 2 | 3 | 4 |
| 十六进制操作码 | 0x01 | 0x02 | 0x03 | 0x04 |
5. 真实世界的 VM 保护器
5.1 VMProtect 分析方法
1. Find VM entry: search for pushad/pushfd sequence
2. Identify VM context structure (registers, flags, bytecode pointer)
3. Locate handler table (often obfuscated with opaque predicates)
4. For each handler:
a. Remove junk code / opaque predicates
b. Identify the core operation
c. Document handler semantics
5. Trace bytecode execution (instruction-level trace)
6. Reconstruct original code from trace
5.2 Tigress 混淆器
可配置保护层的学术 VM 混淆器。
| 特性 | 方法 |
|---|---|
| 单分派 VM | 标准处理器提取 |
| 分离处理器 | 处理器分散在多个函数中 |
| 嵌套 VM | 外层 VM 处理器调用内层 VM |
| 加密字节码 | 每次取指前动态解密 |
| 多态处理器 | 每次构建中,同一操作使用不同代码 |
5.3 常见 VM 保护器模式
| 保护器 | 分派器风格 | 难度 |
|---|---|---|
| VMProtect | 表 + 不透明谓词 | 高 |
| Themida (Code Virtualizer) | 类 CISC,大量处理器 | 高 |
| Tigress | 可配置,学术用途 | 中高 |
| 自定义 CTF VM | 简单 switch | 低至中 |
| Movfuscator | 全部通过 mov 运算 | 中 |
6. 工具
| 工具 | 用途 | 用法 |
|---|---|---|
| IDA Pro | 识别分派器,逆向处理器 | F5 反编译,交叉引用分析 |
| Ghidra | 提供 Sleigh 处理器模块的免费替代工具 | 为 VM ISA 编写自定义处理器 |
| angr | 对 VM 执行符号执行 | 将整个 VM 视为约束系统 |
| Pin / DynamoRIO | 通过动态插桩进行跟踪 | 记录操作码处理器的执行顺序 |
| REVEN | 记录完整系统跟踪 | 重放并分析 VM 执行过程 |
| Unicorn | 模拟 VM 执行 | 快速模拟处理器 |
| Miasm | 基于 IR 的分析 | 将 VM 处理器提升为 IR 以进行分析 |
| 自定义 Python | 编写反汇编器/反编译器 | 为每项挑战定制工具 |
Ghidra Sleigh 处理器模块
对于需要反复分析的 VM 架构,可以编写 Sleigh 处理器规范:
define space ram type=ram_space size=2 default;
define space register type=register_space size=1;
define register offset=0 size=1 [ R0 R1 R2 R3 FLAGS PC SP ];
define token opcode(8)
op = (0,7)
;
:NOP is op=0x00 { }
:PUSH imm is op=0x01; imm { SP = SP - 1; *[ram]:1 SP = imm; }
:POP is op=0x02 { SP = SP + 1; }
:ADD is op=0x03 { local a = *[ram]:1 (SP+1); *[ram]:1 (SP+1) = a + *[ram]:1 SP; SP = SP + 1; }
7. 决策树
Binary contains custom bytecode interpreter?
│
├─ Can you identify the dispatcher?
│ ├─ Yes (switch/table/if-chain)
│ │ ├─ Few opcodes (< 20) → Simple CTF VM
│ │ │ ├─ Stack-based → map push/pop/arithmetic ops
│ │ │ ├─ Register-based → map mov/add/cmp ops
│ │ │ └─ Write disassembler → analyze program → solve
│ │ │
│ │ └─ Many opcodes (50+) → Commercial protector
│ │ ├─ Known protector → use specific deprotection tools
│ │ └─ Custom → trace execution, pattern-match handlers
│ │
│ └─ No clear dispatcher
│ ├─ All-mov instructions → movfuscator
│ ├─ Encrypted bytecode → find decryption, dump after decode
│ └─ Split/distributed handlers → trace execution to find them
│
├─ Is it a maze challenge?
│ ├─ Extract grid from data section
│ ├─ Identify direction encoding
│ ├─ BFS/DFS to find shortest path
│ └─ Convert path to expected input format
│
├─ Is there input validation in VM?
│ ├─ Small input space → brute-force via Unicorn emulation
│ ├─ Known format → constrained angr solve
│ └─ Complex check → write disassembler, analyze check logic
│
└─ Multiple VM layers (VM in VM)?
├─ Analyze outer VM first
├─ Extract inner bytecode
├─ Repeat analysis for inner VM
└─ Consider: symbolic execution may handle nested VMs directly
8. CTF 解题工作流
1. Run the binary — understand I/O behavior
└─ What input does it expect? What output on success/failure?
2. Open in IDA/Ghidra — find the main loop
└─ Look for while/for loop with switch or indirect jump
3. Identify VM components:
├─ Bytecode location (where is the program data?)
├─ PC/IP variable (how is current position tracked?)
├─ Registers/stack (where is VM state stored?)
└─ I/O handlers (which opcodes read input / write output?)
4. Map all opcodes (create the ISA specification)
└─ For each case/handler: opcode number, operation, operands
5. Write disassembler in Python
└─ Output readable assembly for the bytecode
6. Analyze the disassembled program:
├─ Find input reading
├─ Trace transformations applied to input
├─ Find comparison against expected values
└─ Reverse the transformation to find valid input
7. Solve:
├─ If simple transforms (XOR, ADD) → reverse manually
├─ If complex → feed to Z3 as constraints
└─ If maze → extract grid, run pathfinding
评论
支持 Markdown