yaklang/hack-skills1 file

Vm And Bytecode Reverse

>-

Specification
Skill ID
yaklang/hack-skills/vm-and-bytecode-reverse
Publisher
yaklang
Repository
hack-skills
Installs
0
Files
1
Synced
Sep 23, 2026
How to use it

Open any RiverX project, open the Skills panel in the chat, and search for this identifier. The files are fetched from the source repository at install time.

yaklang/hack-skills/vm-and-bytecode-reverseInstalls these files
  • SKILL.md

What this skill tells the agent

SKILL: VM & Bytecode Reverse Engineering — Expert Analysis Playbook

AI LOAD INSTRUCTION: Expert techniques for reversing custom virtual machines and bytecode interpreters. Covers dispatcher identification, opcode mapping, custom ISA reconstruction, disassembler/decompiler writing, maze challenges, and real-world VM protector analysis. Base models often fail to recognize the fetch-decode-execute pattern or attempt to analyze VM bytecode as native code.

0. RELATED ROUTING

  • code-obfuscation-deobfuscation when the VM is a commercial protector (VMProtect/Themida)
  • symbolic-execution-tools when using angr to solve VM-based challenges
  • anti-debugging-techniques when the VM includes anti-debug checks

Quick identification

Binary PatternLikely VM TypeStart With
while(1) { switch(bytecode[pc]) }Switch-based dispatcherMap each case to an operation
Indirect jump via table jmp [table + opcode*8]Table-based dispatcherDump jump table, analyze handlers
Nested if-else chain on byte valueIf-chain dispatcherSame as switch, just different syntax
Stack push/pop dominant operationsStack-based VMIdentify push, pop, arithmetic ops
reg[X] = ... array operationsRegister-based VMMap register indices to operations
2D grid + direction inputMaze challengeExtract grid, apply BFS/DFS

1. CUSTOM VM IDENTIFICATION

1.1 Structural Indicators

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 Signatures

Switch dispatcher (most common in 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;
    }
}

Table dispatcher (more optimized):

typedef void (*handler_t)(vm_ctx_t*);
handler_t handlers[256] = { handle_nop, handle_push, handle_add, ... };

while (running) {
    handlers[bytecode[pc++]](&ctx);
}

2. ANALYSIS METHODOLOGY

Step 1: Find the Dispatcher

Look for:

  • Large switch statement (many cases) in a loop
  • Array of function pointers indexed by a byte from a data buffer
  • Single function with high cyclomatic complexity
  • Cross-references to a data buffer read byte-by-byte

Step 2: Map Opcodes to Operations

For each case/handler, determine:

PropertyHow to Identify
Opcode valueCase number or table index
Operation typeRegister/stack modifications
Operand countHow many bytes consumed after opcode
Operand typeImmediate value, register index, or memory address
Side effectsOutput, memory write, flag modification

Step 3: Extract Bytecode Program

# 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)

Step 4: Write Custom Disassembler

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)

Step 5: Analyze Disassembled Program

With the custom disassembly, apply standard reverse engineering:

  • Identify input reading (read opcode)
  • Trace data flow from input to comparison
  • Determine success/failure conditions
  • Extract the check logic (often XOR/ADD transformations of input compared against constants)

3. COMMON VM PATTERNS IN CTF

3.1 Stack-Based VM

Operations work on a stack (like JVM or Python bytecode).

OpcodeOperationStack Effect
PUSH immPush immediate value[...] → [..., imm]
POPDiscard top[..., a] → [...]
ADDAdd top two[..., a, b] → [..., a+b]
SUBSubtract[..., a, b] → [..., a-b]
MULMultiply[..., a, b] → [..., a*b]
XORBitwise XOR[..., a, b] → [..., a^b]
CMPCompare[..., a, b] → [..., (a==b)]
JMP addrUnconditional jumpno change
JZ addrJump if top is zero[..., a] → [...]
PRINTOutput top as char[..., a] → [...]
READRead char to stack[...] → [..., input]
HALTStop execution-

3.2 Register-Based VM

Operations use register indices (like x86, ARM).

OpcodeFormatOperation
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]
SYSCALL0x0BI/O operation based on reg[0]
HALT0xFFstop

3.3 Brainfuck-like / Esoteric VMs

BF CommandVM EquivalentDescription
>INC ptrMove data pointer right
<DEC ptrMove data pointer left
+INC [ptr]Increment byte at pointer
-DEC [ptr]Decrement byte at pointer
.OUTPUT [ptr]Output byte at pointer
,INPUT [ptr]Input byte to pointer
[JZ forwardJump past ] if byte is zero
]JNZ backJump back to [ if byte is nonzero

4. MAZE CHALLENGES

4.1 Identification

  • Binary reads directional input (WASD, arrow keys, UDLR)
  • 2D array in data section (walls, paths, start, end)
  • Position tracking with x,y coordinates
  • Win condition at specific coordinates

4.2 Map Extraction

# 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 Automated Solving

from collections import deque

def solve_maze(maze, start, end):
    """BFS solver returns direction string."""
    rows, cols = len(maze), len(maze[0])

Use this skill in a project

Open any RiverX project, search the Skills panel for yaklang/hack-skills/vm-and-bytecode-reverse, and install it in one click.

Use this skill