mcpField
V
vm-and-bytecode-reverse

vm-and-bytecode-reverse

github.com/yaklang安全2.6k3.8k 次安装6 周前更新
代码仓库

关于 vm-and-bytecode-reverse

用于分析自定义虚拟机、字节码解释器和 VM 保护二进制文件的逆向工程技能。内容介绍调度器识别、操作码映射、反汇编器编写,以及 CTF 迷宫题和实际保护器的分析方法。

主要功能

  1. 01识别常见 VM 调度器模式
  2. 02映射操作码、操作数和副作用
  3. 03指导提取字节码并编写反汇编器
  4. 04涵盖基于栈和寄存器的 VM
  5. 05提供迷宫求解与保护器分析流程

适用场景

  1. 01逆向分析自定义字节码解释器
  2. 02分析基于 VM 的 CTF 挑战
  3. 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. 相关路由

快速识别
二进制特征可能的虚拟机类型从何处入手
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] → [...]
PRINT将栈顶元素作为字符输出[..., a] → [...]
READ读取字符并压入栈[...] → [..., input]
HALT停止执行-
3.2 基于寄存器的 VM

操作使用寄存器索引(类似 x86、ARM)。

操作码格式操作
MOV r, imm0x01 RR II IIreg[R] = imm16
MOV r1, r20x02 R1 R2reg[R1] = reg[R2]
ADD r1, r20x03 R1 R2reg[R1] += reg[R2]
SUB r1, r20x04 R1 R2reg[R1] -= reg[R2]
XOR r1, r20x05 R1 R2reg[R1] ^= reg[R2]
CMP r1, r20x06 R1 R2flags = compare(r1, r2)
JMP addr0x07 AA AApc = addr
JE addr0x08 AA AAif equal: pc = addr
LOAD r, [addr]0x09 RR AAreg[R] = mem[addr]
STORE [addr], r0x0A AA RRmem[addr] = reg[R]
SYSCALL0x0B根据 reg[0] 执行 I/O 操作
HALT0xFF停止
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 方向编码

不同挑战对方向的编码方式各不相同:

编码上下左右
WASDWSAD
UDLRUDLR
箭头键↑ (0x48)↓ (0x50)← (0x4B)→ (0x4D)
数字1234
十六进制操作码0x010x020x030x04

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