Write-up • WebAssembly • Crypto (AES-CTR)

WatCTF — tfw_no_stack_locals_bg.wasm

Goal. Extract the flag validated by the exported function check_flag. The module transforms the 32-byte input using AES-128-CTR and compares it against a 56-byte constant blob. If we decrypt that blob with the same key/IV, the plaintext is the flag.

AES-128-CTR wasm-bindgen glue data section / i64.const offline reconstruction

Behavior & Findings

Key & IV

Key (data section)

key = b"OOOOHMYFAVOURITE"  # 16 bytes

IV from two i64.const

In .wat, two qwords make the 16-byte IV (little-endian per word):

(i64.const 6869194837183520599)  ; b"_THOSE_W"
(i64.const 5210488070931961695)  ; b"HOKNOW__"

LE bytes → b"_THOSE_W" + b"HOKNOW__"_THOSE_WHO_KNOW_.

The “7 × i64 → 56 bytes” rule

  1. Take the seven consecutive i64.const values as they appear in .wat: Q0 .. Q6.
  2. Reverse the word order to match memory layout: Q6, Q5, …, Q0.
  3. Pack each qword as 8 little-endian bytes and concatenate:
    ciphertext = LE8(Q6) || LE8(Q5) || … || LE8(Q0)

Seven qwords from this module

143009642011427521
6315395457821302550
-1955905064672638357
-8684071750392024005
-8682618338371224816
2570840801305670777
3584201232957687288

Reconstructed ciphertext (hex)

f8c5f50d08a4bd3179b4e77f1676ad23103b17ad9f2481873bf4e0d1c0fa7b876bcaccddb43adbe416df8d5fb5caa457c1028cff8212fc01

Decrypting the blob (AES-128-CTR)

Python (PyCryptodome)

from Crypto.Cipher import AES
from binascii import unhexlify

KEY = b"OOOOHMYFAVOURITE"
IV  = b"_THOSE_WHO_KNOW_"
CT  = unhexlify(
  "f8c5f50d08a4bd3179b4e77f1676ad23"
  "103b17ad9f2481873bf4e0d1c0fa7b87"
  "6bcaccddb43adbe416df8d5fb5caa457"
  "c1028cff8212fc01"
)

pt = AES.new(KEY, AES.MODE_CTR, nonce=b"", initial_value=IV).decrypt(CT)
print(pt.decode())

Node.js

import crypto from 'crypto';

const KEY = Buffer.from('OOOOHMYFAVOURITE', 'ascii');
const IV  = Buffer.from('_THOSE_WHO_KNOW_', 'ascii');
const CT  = Buffer.from(
  'f8c5f50d08a4bd3179b4e77f1676ad23' +
  '103b17ad9f2481873bf4e0d1c0fa7b87' +
  '6bcaccddb43adbe416df8d5fb5caa457' +
  'c1028cff8212fc01', 'hex'
);

const d = crypto.createDecipheriv('aes-128-ctr', KEY, IV);
const PT = Buffer.concat([d.update(CT), d.final()]);
console.log(PT.toString());
Flag: watctf{if_you_look_into_it_w4sm_1s_4ctually_4_b1t_w31rd}

Appendix — locating the pieces

Tip: WASM binaries don’t include runtime memory; if you scan the raw .wasm you won’t see the ciphertext unless you reconstruct it from constants (as we did) or dump memory after execution.

Prepared for a WebAssembly + Crypto reverse challenge. All names belong to their respective owners.