Description
Flag two requires exploiting a WASM-compiled CPU simulator. The nand_checker program validates circuits, but its bounds check can be bypassed to overwrite program instructions mid-execution and trigger the hidden flag routine.
Complete Pachinko (flag one) first to understand the nand_checker circuit format.
Download the server source and decompile the WASM binary. This writeup uses wasm2c plus a text editor for the analysis (Ghidra with the WASM plugin is the GUI alternative if you prefer the navigator).
Identify all port addresses by grepping the decompiled C for repeated 16-bit constants used in load/store pairs - the recurring addresses are PC, data in/out, clock, reset, write enable, halt, and the flag port.
wget https://challenge-files.picoctf.net/c_activist_birds/7eac27979c12e4bd449f03e40a8492044221b7d2a96ac85f1150e30983c56eac/server.tar.gztar -xvf server.tar.gzfile *.wasmwasm2c nand_checker.wasm -o nand_checker.cgrep -oE '0x[0-9a-fA-F]{4}' nand_checker.c | sort | uniq -c | sort -rn | head -20# The most-referenced 16-bit values are the port addressesSolution
Walk me through it- Step 1Reverse the WASM CPU simulatorThe server runs a WASM-compiled Verilog CPU. Convert it to C with
wasm2cand grep for 16-bit constants - the addresses that show up in repeated load/store pairs are the CPU's memory-mapped ports. Reconstruct the CPU's behaviour in Python by tracing state reads/writes; the program counter port increments by 2 each cycle, which makes it the easiest port to identify.bash# Decompile to inspect: wasm2wat nand_checker.wasm > nand_checker.wat wasm2c nand_checker.wasm -o nand_checker.c # Find port addresses by frequency of 16-bit constants: grep -oE '0x[0-9a-fA-F]{4}' nand_checker.c | sort | uniq -c | sort -rn | head # Ports to identify: # - address port (PC, +2 per cycle - look for the pattern <addr> += 2) # - data_in / data_out (read/write paired with the address port) # - write_enable, clock, reset, halt, flagLearn more
WebAssembly (WASM) is a binary instruction format designed for execution in browsers and sandboxed server environments. It is compiled from languages like C, C++, and Rust. Although WASM is binary, it has a well-defined text format (
.wat) and can be decompiled with tools likewasm2wat, Ghidra's WASM loader, or Binary Ninja. The decompiled output is lower-level than the original source but fully recoverable.The CPU implemented in this challenge is a custom instruction set architecture (ISA) - not x86 or ARM, but a purpose-built design with NAND gates, memory ports, and a custom instruction encoding. Identifying the ISA requires correlating port reads and writes with observable behaviour (e.g., the program counter increments predictably), reconstructing the instruction decoder, and disassembling the provided
nand_checkerprogram binary with the discovered ISA.This type of challenge is representative of real-world firmware reverse engineering, where embedded devices run on proprietary processors with undocumented instruction sets. Tools like IDA Pro, Ghidra, and Binary Ninja support custom ISA plugins, and researchers have successfully reversed undocumented CPUs in industrial controllers, automotive systems, and legacy hardware by combining static analysis with dynamic observation.
- Step 2Identify the validation bypass vulnerabilityThe validator checks that each node ID is below 0x1000, but the array index it computes after the check is
node_id * 2. Tying one NAND input to 0xfff inverts the other (NAND truth: NAND(0xfff, 0x000)=0xfff, NAND(0xfff, 0xfff)=0x000). The attacker-chosen 0xfff becomes 0xf000; scaled by 2 plus the inputs base, it wraps modulo 0x10000 to address 0x0000. That is exactly where instructions live, so the bug becomes an arbitrary instruction-memory write.bash# NAND-as-NOT truth table (input A tied to 0xfff): # NAND(0xfff, 0x000) = 0xfff (NAND of 0 = all-ones) # NAND(0xfff, 0xfff) = 0x000 (NAND of 1 = zero) # => output = ~B (bitwise NOT of the variable input) # # Validation pseudocode (post-decompilation): # if node_id >= 0x1000: reject # check on raw input # index = node_id * 2 # transformation AFTER check # inputs[index] = nand_output # write at scaled offset # # Path to OOB: # node 0xfff -> index 0x1ffe (just within bounds) # NAND output 0xfff -> inverted to 0xf000 (16-bit ~) # 0xf000 * 2 = 0x1e000, + base 0x2000 = 0x20000 # 0x20000 mod 0x10000 = 0x0000 = instruction memory baseLearn more
This is an integer overflow leading to out-of-bounds write. The validation operates on the raw node ID (ensuring it is below 0x1000), but the actual memory access uses a different, larger value derived from the validated input. By the time the multiplication happens, the bounds check is already in the past. This pattern - checking one value but using a transformed version - is a classic source of vulnerabilities in bounds-checking code.
The 16-bit address wraparound is the key: the CPU's memory addressing uses 16-bit arithmetic, so adding two large values can wrap around from the top of the address space back to address 0x0000. This puts the write target at the very beginning of memory - where the CPU's program instructions are stored. Overwriting instructions while the CPU is in the middle of executing the validation loop allows the exploit to modify the program's future behaviour without stopping it.
This vulnerability class appears in real embedded systems when developers use small integer types (uint8_t, uint16_t) and forget that arithmetic on those types wraps around. CERT C Secure Coding Standard rule INT32-C and similar guidelines specifically prohibit unchecked arithmetic on integer types of mixed width for this reason.
- Step 3Patch instructions via the OOB write
flag.binships in the challenge resources. Disassembled with the recovered ISA, its bytes are the opcodes for the simulated CPU - a sequence that loads four magic constants into r0-r3 and executes theflag_magicinstruction. The flag port is the 16-bit address that the simulator copies bytes from data_out into when the CPU executesflag_magic; you find it by tracing where the simulator reads the data port and writes it to a memory-mapped output. Construct a circuit so the OOB write overwrites the post-validation instruction stream with flag.bin's bytes. When the CPU runs the injected code, the flag port is set and the server returns flag two.bash# Circuit layout to trigger the OOB write: # 1. Create a NAND gate with one input tied to constant 0xfff # 2. Set the output node to 0xfff (the boundary value) # 3. The NAND inversion: ~(0xfff & input) produces 0xf000 # 4. The memory write lands at instruction address 0x0000 # # flag.bin: provided in the challenge tarball. # Disassemble it with the recovered ISA: # load r0, <magic0> # load r1, <magic1> # load r2, <magic2> # load r3, <magic3> # flag_magic # CPU writes flag_token to the flag port # halt # # Locate the flag port: trace data_out -> ??? in the simulator. # The address that gets the byte right before halt is the flag port.Learn more
This final step is a data-only attack: rather than injecting shellcode into a conventional execution context, the attacker patches the running program's instruction stream. The CPU continues executing normally - it just runs different instructions at the overwritten addresses. This technique is analogous to a hot-patch exploit in embedded firmware: overwrite a function in place while the system is running, so the next call to that function executes attacker-controlled logic.
The
flag.binprogram is provided as part of the challenge resources. Disassembling it with the recovered ISA gives you the raw opcodes to write. The exploit circuit is designed so that when nand_checker processes the circuit, the resulting writes precisely overwrite the correct instruction addresses with the flag.bin bytes. Timing matters: the overwrite must happen during the processing loop, before the loop reaches the instruction area itself.Pachinko Revisited is a showcase challenge combining WebAssembly reverse engineering, custom ISA reconstruction, integer overflow analysis, and instruction patching into a single exploit chain. It reflects the skill set required for advanced embedded and firmware security research, where the target processor may be completely undocumented and every step requires building tools from scratch.
Flag
picoCTF{p4ch1nk0_f146_tw0_...}
Decompile the WASM CPU, find the OOB write via node_id*2 wraparound, craft a circuit that patches instruction memory with flag.bin opcodes, then let the CPU execute to set the flag port.