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.
Setup
Open the challenge URL with DevTools, download the WASM file from the Network tab.
wasm2wat xSAR4.wasm -o xSAR4.watSolution
Want to try it yourself first?
The guided walkthrough reveals hints one step at a time.
Step 1Static analysis: decompile and locate the comparison
ObservationThis is WASM with several transformation layers stacked. Decompile to WAT first and grep for comparison instructions, so the whole pipeline is mapped before any reversal starts.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).bashwasm2wat xSAR4.wasm -o xSAR4.watbashwc -l xSAR4.watbash# Find the comparison: memcmp, byte-wise eq/ne, or strcmpbashgrep -nE 'call \$memcmp|i32\.eq|i32\.ne|call \$strcmp' xSAR4.watWhat didn't work first
Tried: Opening the .wasm binary directly in a hex editor and searching for the flag string
The flag bytes never sit in cleartext inside the binary; they only exist as the output of transformations applied at runtime, so a hex search finds nothing. Decompile to WAT and trace the whole pipeline as readable instructions instead.
Tried: Running strings on the .wasm file to extract printable sequences
strings extracts null- or space-terminated ASCII runs, but WASM stores all its data, string constants included, in a binary section strings cannot parse structurally, so the output is noise and partial identifiers. wasm2wat decodes the data section and instruction stream properly, which lets you 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.
Step 2Dynamic: dump expected bytes via the debugger
ObservationThe transformation pipeline is still opaque after static analysis. Switch to dynamic: set a DevTools breakpoint at the comparison instruction and read the expected bytes straight out of 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).jsconst mem = new Uint8Array(instance.exports.memory.buffer, OFFSET, LENGTH);js// Replace OFFSET and LENGTH with the values you saw on the operand stackjs// at the cmp instruction.jsconsole.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 is named by whatever the page chose: window.instance for some, Module or Module._memory for others, or a local closure variable nothing exposes. Guess wrong and the console returns a ReferenceError. Inspect window first and see 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 address is resolved, so the comparison buffer's offset and length are not yet on the operand stack and linear memory holds nothing useful yet. Put the breakpoint on the specific i32.eq or memcmp call found in the previous step, where both operands already exist.
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 WASMinstancedepends on how the page loads it: some pages bind it towindow, others to a global calledModule, and Emscripten output usesModule._memory. Inspectwindowin 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.
Step 3Reconstruct the flag from memory inspection
ObservationThe breakpoint exposes a memory buffer holding the expected comparison value. Read those bytes, then undo the transformation layers in Python, XOR and permutation, applied in reverse order.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 addresspythonpython3 - <<'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')) EOFWhat 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 what the correct input looks like after the pipeline has run, not the original flag. Submit them verbatim and the WASM transforms them again before comparing, so it fails. Invert each layer, in reverse order, to get back to the plaintext input.
Tried: Applying only one transformation (e.g. XOR) when the binary uses multiple layered transforms
This challenge stacks XOR encoding with a permutation, and possibly further arithmetic on top. Reverse only the XOR and the output stays garbled, because the permutation is still in place. Read the WAT to enumerate every transformation in order, then apply the full inverse sequence backwards.
Learn more
WASM linear memory is a flat array of bytes (a
WebAssembly.Memoryobject backed by anArrayBuffer). 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)readslengthbytes starting atoffset.
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.