mcpField
V
vm-and-bytecode-reverse

vm-and-bytecode-reverse

github.com/yaklangセキュリティ2.6k3.8k 回インストール6 週間前に更新
リポジトリ

vm-and-bytecode-reverse について

独自の仮想マシン、バイトコードインタープリター、VM 保護されたバイナリを分析するリバースエンジニアリングスキルです。ディスパッチャーの特定、オペコードの対応付け、逆アセンブラー作成などの手順を扱います。

主な機能

  1. 01一般的な VM ディスパッチャーのパターン特定
  2. 02オペコード、オペランド、副作用の対応付け
  3. 03バイトコード抽出と逆アセンブラー作成の手順
  4. 04スタック型とレジスター型 VM の解説
  5. 05迷路の解法と保護器分析の手順

活用シーン

  1. 01独自バイトコードインタープリターの解析
  2. 02VM を使う CTF 問題の分析
  3. 03VM 保護されたバイナリの調査

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/yaklang/hack-skills --skill vm-and-bytecode-reverse

インストール後、Claude Code で/vm-and-bytecode-reverseを入力して読み込まれたか確認します。

詳細

SKILL: VM とバイトコードのリバースエンジニアリング — エキスパート分析プレイブック

AI LOAD INSTRUCTION:カスタム仮想マシンとバイトコードインタープリターを解析するためのエキスパート向けテクニックです。ディスパッチャーの特定、オペコードのマッピング、カスタム ISA の再構築、逆アセンブラー/デコンパイラーの作成、迷路チャレンジ、実環境での VM プロテクター分析を扱います。ベースモデルは、フェッチ・デコード・実行のパターンを認識できなかったり、VM バイトコードをネイティブコードとして解析しようとしたりすることがよくあります。

0. 関連するルーティング

簡易識別
バイナリのパターン可能性の高い VM の種類まず行うこと
while(1) { switch(bytecode[pc]) }switch ベースのディスパッチャー各 case を操作に対応付ける
テーブル経由の間接ジャンプ jmp [table + opcode*8]テーブルベースのディスパッチャージャンプテーブルをダンプし、ハンドラーを解析する
バイト値を条件とするネストした if-else チェーンif チェーン型ディスパッチャーswitch と同様に解析する(構文が異なるだけ)
スタックへの push/pop が主な操作スタックベース VMpush、pop、算術演算を特定する
reg[X] = ... 形式の配列操作レジスタベース VMレジスタインデックスを操作に対応付ける
2D グリッドと方向入力迷路チャレンジグリッドを抽出し、BFS/DFS を適用する

1. カスタム VM の識別

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:ディスパッチャーを見つける

次の特徴を探します:

  • ループ内にある、多数の case を持つ大きな switch 文
  • データバッファー内のバイトをインデックスとして使う関数ポインター配列
  • 循環的複雑度が高い単一の関数
  • データバッファーをバイト単位で読み取る箇所へのクロスリファレンス
ステップ 2:オペコードを操作に対応付ける

各 case/ハンドラーについて、次の内容を特定します:

プロパティ識別方法
オペコード値case 番号またはテーブルインデックス
操作の種類レジスター/スタックの変更
オペランド数オペコードの後に消費されるバイト数
オペランドの種類即値、レジスターインデックス、メモリアドレス
副作用出力、メモリ書き込み、フラグの変更
ステップ 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 でよく見られる VM のパターン

3.1 スタックベース VM

操作はスタック上で行われます(JVM や Python バイトコードと同様)。

オペコード操作スタックへの影響
PUSH imm即値をプッシュ[...] → [..., imm]
POP先頭の値を破棄[..., a] → [...]
ADD先頭の 2 値を加算[..., a, b] → [..., a+b]
SUB減算[..., a, b] → [..., a-b]
MUL乗算[..., a, b] → [..., a*b]
XORビット単位の 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]
SYSCALL0x0Breg[0] に基づく I/O オペレーション
HALT0xFF停止
3.3 Brainfuck風/難解言語 VM
BF コマンドVM の相当処理説明
>INC ptrデータポインターを右に移動
<DEC ptrデータポインターを左に移動
+INC [ptr]ポインター位置のバイトをインクリメント
-DEC [ptr]ポインター位置のバイトをデクリメント
.OUTPUT [ptr]ポインター位置のバイトを出力
,INPUT [ptr]ポインター位置にバイトを入力
[JZ forwardバイトがゼロの場合、] の直後へジャンプ
]JNZ backバイトがゼロ以外の場合、[ へ戻る

4. 迷路チャレンジ

4.1 識別
  • バイナリが方向入力(WASD、矢印キー、UDLR)を読み取る
  • データセクションにある 2D 配列(壁、通路、開始地点、終了地点)
  • 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
16 進オペコード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 でデコンパイル、クロスリファレンス解析
GhidraSleigh プロセッサーモジュールを備えた無料の代替ツールVM ISA 用のカスタムプロセッサーを作成
angrVM 内でのシンボリック実行VM 全体を制約システムとして扱う
Pin / DynamoRIOトレース用の動的インストルメンテーションオペコードハンドラーの実行シーケンスを記録
REVENシステム全体のトレースを記録VM の実行をリプレイして解析
UnicornVM の実行をエミュレートハンドラーを高速にエミュレート
MiasmIR ベースの解析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

コメント