WebAssembly (WASM) Reversing
Reverse engineer WebAssembly binaries in CTF challenges using wasm2wat, ghidra, and browser tools.
WebAssembly (WASM) Reversing
WebAssembly (.wasm) is a binary instruction format for stack-based virtual machines, designed to run in browsers at near-native speed. CTF challenges increasingly use WASM for reversing. The key tool is wasm2wat — it converts binary WASM to human-readable text format (WAT), which looks like assembly and is straightforward to analyze.
What Is WASM?
.wasm file → binary format (compact, not human-readable)
.wat file → text format (human-readable, like assembly)
WASM uses a stack machine: instructions push/pop values
Types: i32, i64, f32, f64Initial Analysis
# Identify
file challenge.wasm
# → WebAssembly (wasm) binary module
# View magic bytes
xxd challenge.wasm | head -3
# → 00 61 73 6d = "\0asm" (WASM magic)
# Extract strings
strings challenge.wasm
strings -n 6 challenge.wasm | grep -iE "(flag|key|pass|ctf)"Disassembly: wasm2wat
Convert binary WASM to human-readable WAT format:
# Install wabt (WebAssembly Binary Toolkit)
sudo apt install wabt
# Convert .wasm to .wat
wasm2wat challenge.wasm -o challenge.wat
# View the WAT
cat challenge.wat | head -100WAT looks like:
(module
(func $check_flag (param $input i32) (result i32)
local.get $input
i32.const 42
i32.xor
i32.const 0x666c6167
i32.eq
)
(export "check_flag" (func $check_flag))
)Decompilation: Ghidra + WASM Plugin
Ghidra supports WASM with a plugin (or built-in support in Ghidra 11+):
- Import
.wasmfile into Ghidra - Auto-analyze → decompile
- Navigate to exported functions
Prefer Ghidra's built-in WASM support (Ghidra 11+) over third-party plugins. Some third-party plugins are not actively maintained.
Browser-Based Analysis
WASM challenges often run in the browser. Use DevTools:
// In browser console (F12):
WebAssembly.instantiateStreaming(fetch("/challenge.wasm")).then((result) => {
const exports = result.instance.exports;
console.log(exports); // list all exported functions
// Call the check function with different inputs
console.log(exports.check(0x666c6167));
});Hooking WASM Functions
const memory = new WebAssembly.Memory({ initial: 1 });
const imports = {
env: {
memory,
check_answer: function (ptr, len) {
const bytes = new Uint8Array(memory.buffer, ptr, len);
console.log("Input:", new TextDecoder().decode(bytes));
},
},
};Common WASM CTF Patterns
XOR in WAT
; This XORs local variable with constant
local.get $input
i32.const 0x42
i32.xor
; → input XOR 0x42expected = [0x27, 0x26, 0x23, ...] # from WAT constants
print(''.join(chr(b ^ 0x42) for b in expected))Linear memory strings
# Strings in WASM linear memory start at a data offset
# Find in WAT: (data (i32.const 0x100) "CTF{...")
import re
with open('challenge.wasm', 'rb') as f:
data = f.read()
flags = re.findall(rb'flag\{[^\}]+\}', data)
print([f.decode() for f in flags])Wabt Tools Reference
wasm2wat input.wasm -o output.wat # binary → text
wat2wasm input.wat -o output.wasm # text → binary
wasm-validate challenge.wasm # validate
wasm-objdump -d challenge.wasm # disassembly
wasm-objdump -x challenge.wasm # section headers
wasm-objdump -s challenge.wasm # section contents
wasm-strip challenge.wasm -o stripped.wasm # strip debug infoChecklist
-
strings challenge.wasm | grep -i flag(quick win) -
wasm2wat→ read WAT for obvious flag checks -
wasm-objdump -dfor disassembly - Ghidra with WASM plugin for decompilation
- Browser DevTools → instantiate module, call functions directly
- Hook imports to intercept memory accesses
- Look for XOR constants, comparison constants in WAT
Last updated on