A Simple Stack VM?
A Simple Stack VM?
Platform: hackerlab | Category: Reversing | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2026-04-05 | Status: Solved Techniques: opcode_recovery, transform_inversion, vm_bytecode_analysis
Summary
Task: a stripped Linux ELF implements a custom stack-based VM that validates a flag through embedded bytecode and a hidden target table. Solution: recover the opcode meanings from the interpreter, isolate the per-byte transform, invert it, and decode the full CODEBY flag offline.
A Simple Stack VM? — Hackerlab
Description
A simple stack VM?
English summary: we are given a stripped 64-bit Linux ELF and need to reverse its custom virtual machine to recover the correct CODEBY{...} flag.
Recon
The title hint is important: VM here means a custom virtual machine, not VirtualBox or VMware.
Initial triage showed that vm.elf is a stripped 64-bit Linux ELF PIE with NX and Full RELRO. Useful strings immediately suggested a stack-machine design:
Full stackStack emptySTACK[%hu]: %huIt's that flag!! :)This is a false flag :(How about checking your flag? :)You need to spell the flag!- input prompts for
C,O, andD
The two main areas worth reversing were:
- the opcode dispatcher / interpreter around
0x11dd - the main VM loop around
0x1aa0
The embedded VM bytecode lives in .rodata near 0x2020, and the table used during validation is stored in .data at 0x40e0.
VM Analysis
Following the interpreter showed that the program does not execute native logic for the flag check. Instead, it reads 16-bit words from the bytecode stream and dispatches them as VM opcodes.
The most useful step was to label opcodes by watching how each handler manipulated the VM state: stack top, instruction pointer, comparison flag, and a data-table index. Strings such as Full stack, Stack empty, and STACK[%hu]: %hu made the stack semantics obvious, while the success and failure strings identified output opcodes.
The recovered opcode mapping used by the challenge program was:
0x1315=PUSH imm160x1316=ADD0x1318=XOR0x1320=POP0x1321=SUB(b - a)0x1533= compare low byte, set flag0x1534= conditional jump if true0x1535= conditional jump if false0x2835= push next word from.data:0x40e00x1400= read char0x1406= print intro0x40ab= print false message0x40ac= print success message0x1337= stop VM
That mapping was enough to understand the full verification program in .rodata.
Extracting the Check
Once the bytecode was interpreted symbolically, the flag check reduced to a simple per-character loop.
For each input byte ch, the VM performs:
text read-only • copy allowed((ch + 0x33) ^ 0xBB) - 0x13
Then it compares the low byte of the result with the next constant loaded by opcode 0x2835 from .data:0x40e0.
The embedded target table is:
text read-only • copy allowedba 26 b9 b0 bb 24 02 2a 14 08 05 b1 b0 16 0d b5 11 0f 16 1f 28 f8
The opcode meanings become clear from the structure of the bytecode itself:
READ_CHARpushes the next user byte.PUSH imm16 0x33,ADDmatchesch + 0x33.PUSH imm16 0xBB,XORmatches^ 0xBB.PUSH imm16 0x13,SUBmatches subtracting0x13.PUSH_DATA, then the compare opcode, matches checking the transformed byte against the table.- Conditional jumps choose either the failure path (
0x40ab) or continue the loop until the success path (0x40ac).
So even though the binary looks like a VM challenge, the core check is just a fixed reversible byte transform applied to each flag character.
Solving
To recover the original character, invert the transform:
text read-only • copy allowedch = (((target + 0x13) ^ 0xBB) - 0x33) & 0xFF
Applying that to every byte in the .data:0x40e0 table yields:
text read-only • copy allowedCODEBY{REDACTED}
The helper script already present in the task directory, tasks/hackerlab/a_simple_stack_vm/solve_vm.py, implements exactly this inversion. A minimal equivalent solver is:
python read-only • copy allowed#!/usr/bin/env python3 table = [ 0xBA, 0x26, 0xB9, 0xB0, 0xBB, 0x24, 0x02, 0x2A, 0x14, 0x08, 0x05, 0xB1, 0xB0, 0x16, 0x0D, 0xB5, 0x11, 0x0F, 0x16, 0x1F, 0x28, 0xF8, ] def invert_byte(x: int) -> int: return (((x + 0x13) ^ 0xBB) - 0x33) & 0xFF flag = "".join(chr(invert_byte(x)) for x in table) print(flag)
Expected output:
text read-only • copy allowedCODEBY{REDACTED}