$ / 2 min read/rev
nitro
The binary decrypts its own checker at runtime and reuses those decrypted bytes as the flag keystream. Solved statically — the binary never runs, so the anti-debug never fires.
- section
- CSAW26
- event
- ctf.csaw.io ↗
- category
- rev
- status
- ● solved
Summary
The binary decrypts its own secret_check function at runtime and then uses
those same decrypted bytes as the keystream for the flag — the code is its own
key material. Solved entirely statically; the binary is never executed, so no
anti-debug is ever engaged.
Chain
1. Self-modifying code
main() calls mprotect on its own .enccode section to make it RWX, then
XOR-decrypts 0x15e bytes at secret_check with an 8-byte repeating key
1337c0debaadf00d (symbol smc_key.0), and calls the result.
Replicating that offline gives the decrypted function body.
2. Password recovery
The decrypted secret_check builds its expected password on the stack one byte
at a time as mov BYTE PTR [rbp+disp8], imm8 — opcode c6 45 <disp8> <imm8>.
Regex the decrypted bytes for that pattern, sort by stack displacement, and take the longest run of consecutive slots holding printable values:
slots = sorted({d[0]: v[0] for d, v in re.findall(rb'\xc6\x45(.)(.)', dec, re.S)}.items())
→ n2o_boost
3. Flag keystream
The flag is not stored. It is XORed against a keystream indexed back into the decrypted code:
k_i = (dec[(7*i + 3) % 0x15e] + 5*i + 0x6b) mod 256
flag[i] = enc_flag[i] ^ k_i # over 0x2f bytes
Note
Because the keystream is derived from the decrypted code, you cannot simply dump the flag at runtime without also reconstructing the SMC step — and reconstructing the SMC step makes running the binary unnecessary.
Solver: ~/ctf/work/nitro/solve.py
## challenge files
26 files · 115 KB- nitro
- nitro.dec
- nitro.dec_ghidra/
- nitro.decomp.c
- nitro_ghidra/
- patch.py
- secret.c
- solve.py