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.
watctf{<24>}
. Wrong length hits an early br_if
and skips the interesting path.OOOOHMYFAVOURITE
(ASCII, 16 bytes) — embedded in data._THOSE_WHO_KNOW_
(ASCII, 16 bytes) — written as two 64-bit words.AES-128-CTR(key, iv)(flag) == ciphertext_56
. Decrypt the 56-byte blob with the same key/IV → plaintext is the flag.key = b"OOOOHMYFAVOURITE" # 16 bytes
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_
.
i64.const
values as they appear in .wat
: Q0 .. Q6
.Q6, Q5, …, Q0
.ciphertext = LE8(Q6) || LE8(Q5) || … || LE8(Q0)
143009642011427521
6315395457821302550
-1955905064672638357
-8684071750392024005
-8682618338371224816
2570840801305670777
3584201232957687288
f8c5f50d08a4bd3179b4e77f1676ad23103b17ad9f2481873bf4e0d1c0fa7b876bcaccddb43adbe416df8d5fb5caa457c1028cff8212fc01
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())
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());
watctf{if_you_look_into_it_w4sm_1s_4ctually_4_b1t_w31rd}
.wat
for the seven consecutive i64.const
that populate the 56-byte buffer.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.