zwesty
Write-ups / Day 16 / Binary
Day 16 · Fri, Oct 16 · Binary

Wasm Up

Prey slaughtered

WebAssembly binary format (sections, LEB128, memory limits), static analysis, regression-safe solver changes

STTMClaude Homebase Both

Flags are shortened to their first 8 and last 4 characters because they're unique per team.

Challenge ID
binary-reverse-engineering-day-16-webassembly-optimization-equivalence (+ /time_trial, /mastery)
Solved
2026-10-16, all three modes, 12 min 55 s from the first start (13:01:40 → 13:14:35 UTC)
Player
Zwesty (solo), with Claude Code (Opus 5.5): our automated morning solver (a scheduled script on our Kali VM), then a Claude helper agent

The challenge

A WebAssembly "sandbox doors" theme: map a module's imports, memory, and indirect calls, and inventory what passes through. As every Binary day, the question set compounds: all earlier days' fields (hashes, ELF/PE/JAR facts, the HCFG config record) plus Day 16's new ones. Four were new types: d16_analysis_{direct,compact}_{format,architecture}.

Reading the prompt: what it said we'd need

Clue What it pointed to What I did about it
"WebAssembly", sandbox "doors" A .wasm module: imports, memory, tables are its only ways out Walked the sections statically with wabt
"optimization equivalence" Two builds of the same logic Compared direct and compact: same function, same config
format / architecture fields The container word and the target, by analogy with elf / x86-64 Ranked candidates before sending

Safety

Static only: wasm-objdump, wasm2wat, and a small Python section walker on our Kali VM. Nothing was executed. Artifacts stayed on the VM.

What was in the files

Two small WebAssembly version-1 modules (d16--variant-000-direct.wasm, 570 bytes; …-compact.wasm, 561 bytes). Sections Type, Function, Memory, Export, Code, Data; no imports, no table (so no indirect calls), one linear memory (initial 1 page, no 64-bit flag), exports memory and evaluate (i32 → i32). Data segment 0 holds the usual HCFG config record (XOR-encoded, CRC-checked); segment 1 a trace label. direct computes its result with a nested if/else chain, compact with branch-free selects: the "optimization equivalence".

Single Solve

  • Our automated morning solver (13:01:40–13:01:46): everything the existing solver knew, in three batches (141 + 9 + 6 answers), all accepted. Four questions left; the solver stopped cleanly.
  • Claude helper agent: read both modules, ranked the candidates (format wasm, then webassembly; architecture wasm32, then wasm, webassembly), and sent the first-ranked ones.
  • 13:05:14 format wasm ×2: accepted.
  • 13:05:23 architecture wasm32 ×2: CORRECT (flag 815014cd…504e).
  • Solver change (our shared binary solver): a WebAssembly reader that checks the header, walks every section by its size, and reads memory limits from imported and defined memories. wasm32, or wasm64 when the 64-bit memory flag is set; anything ambiguous or truncated returns nothing rather than a guess. Tests 10/10 (4 new).

Time Trial and Mastery

We decided to run these before the full regression check, for speed. The quick checks passed first: unit tests, the updated solver reproduced all 160 accepted Single Solve answers, Mastery delivery N: names worked, and the solver's fast path was disabled for the new question ids.

Time Trial (ranked, 80 s clock)
start 1081 ms · artifact 916 ms · solve 734 ms · submit 1344 ms · TOTAL 4075 ms
result CORRECT  flag 27104534…38ca

Mastery (ranked, streamed, 5 deliveries)
solve 1511 ms · submit 1785 ms · TOTAL 6535 ms
result CORRECT  flag 0bef1f7e…233b

Afterwards: the full regression check (65 saved Binary sets: 62 identical, today's 3 only gained the new answers, 0 changed) and a regression rehearsal (worst set 18.4% of its clock) passed, and the change was made permanent in the solver.

Time to solve

All three modes were solved by 13:14:35 UTC (09:14:35 ET), 12 min 55 s after the first start. The Single Solve went through at 13:05:23 with no rejections; about seven minutes then went to a full regression check (which ran out of memory once) before we switched to running the timed modes first, and the ranked Time Trial and Mastery each went through on the first submit.

Takeaways

  • Run the timed modes before the full regression check. The check cost about 9 minutes of the finish, which decided the day's rank (48th; ~30th otherwise). The order is now: Single Solve CORRECT, quick checks, timed runs with the updated solver, then the full regression check.
  • New containers bring the same field types. Format and architecture come back whenever Binary adds a container; pre-building those handlers (with ranked candidates) lets the solver finish the lane by itself.
  • Two parallel regression checks ran the VM out of memory (7.7 GB). One process per set, one at a time.