Athena Wiki

WebAssembly (WASM) Reversing

reversingintermediate

Reverse engineer WebAssembly binaries in CTF challenges using wasm2wat, ghidra, and browser tools.

reversingwasmwebassemblywatbrowserghidrawasm2wat

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, f64

Initial 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 -100

WAT 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+):

  1. Import .wasm file into Ghidra
  2. Auto-analyze → decompile
  3. 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 0x42
expected = [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 info

Checklist

  • strings challenge.wasm | grep -i flag (quick win)
  • wasm2wat → read WAT for obvious flag checks
  • wasm-objdump -d for 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

On this page