Some Assembly Required 4 picoCTF 2021 Solution

Published: April 2, 2026

Description

WASM with more complex obfuscation. Multiple layers of transformation are applied to your input before the comparison. Reverse all layers to find the flag.

Remote

Open the challenge URL with DevTools, download the WASM file from the Network tab.

bash
wasm2wat xSAR4.wasm -o xSAR4.wat

Solution

Want to try it yourself first?

The guided walkthrough reveals hints one step at a time.

Walk me through it
  1. Step 1
    Static analysis: decompile and locate the comparison
    Observation
    I noticed the challenge involves WASM with multiple transformation layers, which suggested decompiling to WAT first and grepping for comparison instructions to map the full pipeline before attempting any reversal.
    Try static analysis first - it is often faster and tells you exactly what the WASM is doing. Decompile to WAT, then search for the comparison instruction the validator uses. If after 10 minutes the transformation pipeline is still opaque, escalate to dynamic analysis (next step).
    bash
    wasm2wat xSAR4.wasm -o xSAR4.wat
    bash
    wc -l xSAR4.wat
    bash
    # Find the comparison: memcmp, byte-wise eq/ne, or strcmp
    bash
    grep -nE 'call \$memcmp|i32\.eq|i32\.ne|call \$strcmp' xSAR4.wat
    What didn't work first

    Tried: Opening the .wasm binary directly in a hex editor and searching for the flag string

    The flag bytes are never stored in cleartext inside the WASM binary - they exist only as the output of inverse transformations applied at runtime. A hex search returns nothing useful. The correct approach is to decompile to WAT so you can trace the full transformation pipeline as readable instructions.

    Tried: Running strings on the .wasm file to extract printable sequences

    strings extracts null-terminated or space-terminated ASCII runs, but WASM encodes all its data - including string constants - in a binary section that strings cannot parse structurally. The output is mostly noise or partial identifiers. wasm2wat correctly decodes the data section and instruction stream so you can grep for the comparison opcode by name.

    Learn more

    Some Assembly Required 4 typically combines XOR encoding, character permutation, and possibly additional arithmetic transformations. The static-first approach is: decompile the WASM, trace the transformation pipeline in order, then apply the inverse transformations in reverse order to the expected output value. The grep above pulls every plausible comparison primitive in one pass; the validation function is almost always one of those.

  2. Step 2
    Dynamic: dump expected bytes via the debugger
    Observation
    I noticed the WAT transformation pipeline was still opaque after static analysis, which suggested switching to dynamic analysis and setting a DevTools breakpoint at the comparison instruction to read the expected bytes directly from WASM linear memory at runtime.
    Open Chrome DevTools > Sources > find the WASM module. Set a breakpoint at the comparison instruction located in the previous step. Submit any input; when the breakpoint hits, read the comparison buffer out of WASM linear memory using the DevTools console.
    js
    // In the DevTools console while paused at the breakpoint:
    js
    // 'instance' is exposed by the page (or look in window.* / Module).
    js
    const mem = new Uint8Array(instance.exports.memory.buffer, OFFSET, LENGTH);
    js
    // Replace OFFSET and LENGTH with the values you saw on the operand stack
    js
    // at the cmp instruction.
    js
    console.log(Array.from(mem));
    What didn't work first

    Tried: Looking for 'instance' in the DevTools console by typing it directly without inspecting window first

    The WASM instance variable name depends on how the page loads it - some pages bind it to window.instance, others to Module, Module._memory, or a local closure variable. Typing 'instance' in the console returns ReferenceError if the page used a different name. The correct approach is to inspect window in the console first to find what the page actually exported.

    Tried: Setting the breakpoint on the WASM function entry instead of the specific comparison instruction

    Breaking at function entry pauses before any memory addresses are resolved, so the comparison buffer offset and length are not yet on the operand stack. You cannot read useful data from linear memory at that point. The breakpoint must be placed on the specific i32.eq or call $memcmp instruction identified in the previous step, where both operands are already materialized.

    Learn more

    Browser WASM debugging is extremely powerful for reverse engineering. Chrome and Firefox both support stepping through WASM instructions, setting breakpoints, and inspecting linear memory. When the WASM comparison function runs, the expected bytes are loaded into memory and compared to your transformed input - at that moment, reading the memory at the comparison address reveals the target value directly.

    Accessing memory. The console expression is new Uint8Array(instance.exports.memory.buffer, offset, length). The handle to the WASM instance depends on how the page loads it: some pages bind it to window, others to a global called Module, and Emscripten output uses Module._memory. Inspect window in the console to find the right handle.

    Workflow split. Run dynamic analysis only to dump the raw expected-comparison bytes. Then leave the browser, decode in Python (apply any remaining inverse transformations identified statically), and submit the recovered string in the browser. This separates "observe one value" (browser) from "compute the inverse" (Python script) and is much faster than trying to do everything inside DevTools.

  3. Step 3
    Reconstruct the flag from memory inspection
    Observation
    I noticed the breakpoint revealed a memory buffer holding the expected comparison value, which suggested reading those bytes and applying Python-based inverse transformations (reversing XOR and permutation layers in reverse order) to recover the original plaintext flag.
    From the breakpoint, read the bytes at the expected-value memory address. This gives you the final transformed value. If needed, apply inverse transformations using Python. Otherwise the memory may already contain the flag in plaintext.
    bash
    # In browser console after hitting breakpoint:
    # Read WASM linear memory as a Uint8Array
    # and decode the bytes at the expected address
    python
    python3 - <<'EOF'
    # If additional decoding is needed after extracting bytes from WASM memory:
    raw_bytes = bytes([...])  # bytes from WASM memory
    # Apply inverse transformations identified from static analysis
    # ...
    print(raw_bytes.decode('ascii', errors='replace'))
    EOF
    What didn't work first

    Tried: Submitting the raw bytes from memory directly as the flag without applying inverse transformations

    The bytes in the comparison buffer are the expected value after the transformation pipeline has been applied to the correct input - they are the transformed form, not the original flag. Submitting them verbatim fails because the WASM applies the same transformations again before comparing. You must invert each transformation layer (in reverse order) to recover the original plaintext input.

    Tried: Applying only one transformation (e.g. XOR) when the binary uses multiple layered transforms

    Some Assembly Required 4 stacks XOR encoding with permutation and possibly additional arithmetic. Reversing only the XOR layer yields garbled output because the permutation step has not been undone. Static analysis of the WAT output is needed to enumerate every transformation in order, so you can apply the full inverse sequence in reverse.

    Learn more

    WASM linear memory is a flat array of bytes (a WebAssembly.Memory object backed by an ArrayBuffer). You can read it directly from JavaScript in the browser console using the memory export. In the DevTools console, after pausing at a breakpoint: new Uint8Array(wasmModule.instance.exports.memory.buffer, offset, length) reads length bytes starting at offset.

Interactive tools
  • Regex TesterTest regular expressions against a string with live match highlighting, flag toggles, and common CTF pattern shortcuts.

Flag

Reveal flag

picoCTF{b9da2135...}

Dynamic analysis via the browser's WASM debugger reveals the expected comparison value at runtime - bypassing even complex multi-layer static obfuscation without needing to reverse it mathematically.

Key takeaway

Dynamic analysis sidesteps static obfuscation entirely by observing program state at the moment of interest rather than reconstructing what the code computes. Setting a debugger breakpoint at the comparison instruction exposes both operands directly in memory, making the complexity of all preceding transformations irrelevant. The same technique applies to native binaries under gdb, Android apps under Frida, and any other environment where you can pause execution and inspect live memory at the point of a secret comparison.

Related reading

Want more picoCTF 2021 writeups?

Useful tools for Web Exploitation

What to try next