Skip to content

Repository files navigation

Wemby — Hunting for Memory Corruption in WebAssembly

Artifact for the ISSTA 2025 paper "Wemby's web: Hunting for Memory Corruption in WebAssembly" (10.1145/3728937).

It analyzes a live site's WebAssembly, flags the functions reachable from attacker-controlled input, and fuzzes them with AFL++ under shadow-memory corruption oracles. The stages are decoupled and communicate only through the databases, keyed by page id (pid) — analyze once, fuzz a pid later.

@ARTICLE{Draissi2025-vc,
  title        = {Wemby’s web: Hunting for Memory Corruption in {WebAssembly}},
  author       = {Draissi, Oussama and Cloosters, Tobias and Klein, David and
                  Rodler, Michael and Musch, Marius and Johns, Martin and Davi,
                  Lucas},
  journaltitle = {Proceedings of the ACM on Software Engineering},
  publisher    = {Association for Computing Machinery (ACM)},
  date         = {2025-06-22},
  articleno    = {ISSTA059},
  doi          = {10.1145/3728937},
  url          = {https://dl.acm.org/doi/10.1145/3728937},
}

Prerequisites

Podman + podman compose. Nothing else on the host — Node, Rust, wabt, AFL++ and Python all run inside the containers. make is optional (see the table below for the raw commands).

For analyze only: fetch the Foxhound browser once.

make foxhound          # or: ./browser/get-foxhound.sh

The ~83 MB binary is a GitHub Release asset, not stored in git; the script verifies its SHA-256, unpacks into browser/foxhound/, and is idempotent. See browser/README.md for the FOXHOUND_URL / FOXHOUND_SHA256 overrides. To use a build that lives elsewhere: FOXHOUND_PATH=/path/to/foxhound/foxhound make analyze url=…. (setup, fuzz and fuzz-example do not need Foxhound.)

Run it

cp .env.example .env                              # one-time; Compose reads it

make setup                                        # 1. start backing services
make analyze url=https://example.com              # 2. analyze a site's WASM
make fuzz pid=<pid> name=<label>                  # 3. fuzz a crawled page id
  1. setup — MariaDB (_wemby), MongoDB (wemby-findings) and the findings collector; schema auto-seeded, all healthchecked.
  2. analyze — drives Foxhound, instruments every WebAssembly module the page loads, records calls/imports/snapshots into _wemby (taint findings go to MongoDB), then runs the taint-analysis stage that flags the fuzzable functions. Prints the page id (pid) you need for step 3.
  3. fuzz — rebuilds that page's modules into a wasm2c + AFL++ harness and fuzzes it. name labels the output dir (defaults to the pid).

No database, just want to see fuzzing work:

make fuzz-example                                 # fuzz example/twitch.tv (no DB)

Tear everything down:

make clean                                        # ⚠ removes containers + volumes

makepodman compose

make equivalent
make setup podman compose up -d
make analyze url=… podman compose --profile analyze run --rm analyzer --url=… then … run --rm analysis
make analyze-gui url=… podman compose --profile analyze-gui run --rm analyzer-gui --gui --url=… then … run --rm analysis
make fuzz pid=… name=… podman compose --profile fuzz run --rm fuzzer <pid> <name>
make fuzz-example podman compose --profile fuzz-example run --rm fuzzer-example
make clean podman compose down -v

Profile-gated services need the explicit --profile <name>: podman compose does not auto-activate a service's profile from run the way Docker Compose v2 does.

Interactive analysis (headful, no time limit)

For sites that need a human to drive them (log in, click around, trigger the WASM-heavy flows), analyze headful instead of the headless ANALYZE_SECONDS dwell — instrumentation continues until you close the window:

make analyze-gui url=https://example.com

Needs a host X server (X11 or Xwayland); make analyze-gui forwards DISPLAY, mounts the X11 socket, and runs the container browser as your host user (userns_mode: keep-id) so it can open the display under rootless podman. If your X server still refuses, run xhost +local: once.

To skip the X11 plumbing, run the analyzer on the host against the setup services on their published 127.0.0.1 ports:

cd analyzer && npm install
FOXHOUND_PATH=../browser/foxhound/foxhound node analyze.js --gui --url=https://example.com

--gui implies wait-for-close; pass --waitClose (or WAIT_FOR_CLOSE=1) for the same unlimited-time behavior headless.

Finding a pid

make analyze prints the new page id as it works. If you lose it, list crawled pages (the pages table stores host/path, not one URL column):

podman compose exec mariadb \
  mariadb -uwemby -pwemby _wemby \
  -e "SELECT pid, host, path, status, finished FROM pages ORDER BY pid DESC;"

Crashes

Output goes to ./fuzzer-output (FUZZ_OUTPUT_DIR); crashes for each fuzzed function land in:

fuzzer-output/<name>/<name>_0/fuzz/<fn>_fuzz/default/crashes/

A saved crash is one of two kinds:

  • sig:11 (SIGSEGV) — an out-of-bounds linear-memory access trapped by the wasm-rt guard page (a wild / off-the-end pointer).
  • sig:06 (SIGABRT) — a shadow-memory oracle detection: the access hit a poisoned byte. Prints BUG FOUND THROUGH WAT ORACLES, a REASON: (BAD HEAP ACCESS, USE AFTER FREE, STACK BUFFER OVERFLOW (redzone), …) and a symbolized backtrace, then aborts.

Turn crashes into self-contained reproducible HTML (runs inside the fuzzer image, no host Python needed):

podman compose --profile fuzz-example run --rm --entrypoint python3 fuzzer-example \
  generate_html.py src/<name>
cd fuzzer-output/<name> && python3 -m http.server   # then open /html

Each build-<fn>.sh also has a repro-<fn>.sh that rebuilds the harness as a single-shot driver with the oracle logging on and re-drives the captured call:

podman compose --profile fuzz run --rm --entrypoint bash fuzzer -c \
  'cd /fuzzer/src/twitch.tv/twitch.tv_0/fuzz && ./repro-f1414.sh'

The bundled twitch.tv capture is a benign recorded call, so it ends in a wasm trap and the driver exits cleanly. A captured input that writes into a redzone stops with BUG FOUND THROUGH WAT ORACLES and aborts. See fuzzer/README.md.

Fuzz variants & budget

Each interesting function is built three ways so you can A/B the technique: full (default — analysis-guided, snapshot-restored), no_analysis, and no_snapshot. Set FUZZ_VARIANT (full | no_analysis | no_snapshot) and AFL_V (seconds per target) in .env.

Repository layout

Path What it is
Makefile, docker-compose.yml, .env.example The 3-step UX: thin make wrapper over the Compose pipeline.
analyzer/ Single-URL WebAssembly analyzer (Node + Playwright + Foxhound) → MariaDB _wemby.
analysis/ Taint-analysis stage (Rust + sqlx): correlates captured call args with the page's attacker-controllable inputs → wasm_parameters + is_interesting.
fuzzer/ Harness synthesis from captured runtime data + AFL++ (via wasm2c). Ships a self-contained example/twitch.tv that runs without a database.
instrumentation/ Binary-only WebAssembly instrumentation engine (Rust); provides the shadow-memory oracles.
findings/ Findings collector — Express + MongoDB, ingests Foxhound's taint findings.
seed/ MariaDB grants + _wemby schema applied on first setup.
browser/ get-foxhound.sh — downloads + verifies the Foxhound build (make foxhound).

About

Dynamic WebAssembly analysis and coverage-guided fuzzing of live websites. Artifact for the ISSTA 2025 paper "Wemby's web: Hunting for Memory Corruption in WebAssembly".

Topics

Resources

Stars

2 stars

Watchers

4 watching

Forks

Releases

Packages

Contributors

Languages