$ cat writeup.md…
$ cat writeup.md…
UIUCTF 2026
Task: AArch64 firmware for a 'ranked console' plus an encrypted RankedFS disk; reconstruct the committed (post-rollback) journal timeline and defeat a RAID//9 dataflow VM to unlock a hidden achievement. Solution: reimplement the journaling FS offline, derive BLAKE2s keystream file keys, emulate the VM and solve the 48-byte player code with bitwuzla (modeling op9 as a symbolic checkpoint/rollback If), then ChaCha20-Poly1305-decrypt achievement.bin.
Silver Wolf calls it a one-button clear. The ranked server calls it an impossible replay. We recovered the firmware from her LV.999 console and the drive containing her last match. Restore the committed timeline and claim the achievement she left behind. The server remembers every rollback. Only committed ticks affect MMR.
Boot command:
qemu-system-aarch64 -M virt -cpu cortex-a72 -m 128M -global virtio-mmio.force-legacy=false -bios godmode.rom -drive file=ranked.img,format=raw,if=none,id=ranked -device virtio-blk-device,drive=ranked -nographic
English summary: Two files are given — godmode.rom (13672-byte raw AArch64 firmware) and ranked.img (16 MB disk, superblock magic RNK9). The firmware is a bare-metal "ranked console" that mounts a custom journaling filesystem (RankedFS), decrypts per-file blocks, and gates an achievement behind a verify command that runs a RAID//9 dataflow VM over four replay files. The goal is to reconstruct the committed (post-rollback) timeline, recover the 48-byte player code the VM accepts, and decrypt the hidden achievement to obtain the flag.
godmode.rom: raw AArch64 firmware loaded by QEMU -bios (entry PC=0, base 0x0). PL011 UART at 0x09000000, virtio-mmio scanned from 0x0a000000.ranked.img: 16 MB disk, superblock magic RNK9, block bodies look encrypted/random.GODMODE//999 RANKED CONSOLE. achievement → LOCKED // restore the committed LV.999 replay; verify prompts PLAYER CODE (48 bytes):. Strings reveal RankedFS tags (RANKEDFS-COMMITTED/ROOT/BLOCK), RAID9 tags (RAID9-MAP/TARGET/DROP), ACHV999, file paths (/profile/player.dat, /replays/{tutorial,placement,promotion,godmode}.raid, /cache/achievement.bin), and the win string ONE-BUTTON CLEAR // HIDDEN MMR 999.reset 0x0, uart_gets 0x80, main 0x140, sb_verify 0x500, uart putc/getc 0x5b4/0x5d4, puts 0x600, virtio_blk_init 0x840, virtio_read_sector 0x984, rankedfs_mount 0xa40, fs_lookup 0x1020, fs_read(+decrypt) 0x10a0, raid9_validate 0x12a0, derive_key 0x1440, verify_part1 0x1500, VM dispatch 0x18d8, VM opcode handlers 0x1c60-0x1e64, verify_part2 0x1e70, cmd_replay 0x2010, BLAKE2s init/update/final 0x2534/0x2590/0x2654, BLAKE2s G-round 0x21c0, ChaCha20 block 0x2780, Poly1305 0x2ad0, ChaCha20-Poly1305 AEAD 0x2dd0.
0x0 (gen0, committed_generation=0) and 0x1000 (gen28, committed_generation=28). Struct: magic RNK9, ver=9, hdr_len=0x3c, journal_start_sector=2, journal_count, committed_generation@+0x14, FNV-1a checksum@+0x18 (init 0x811c9dc5, prime 0x01000193, per byte x ^= x>>13, over 0x3c bytes with the checksum word zeroed).0x2000, 0x80-byte entries, magic JRNL, strictly increasing seq, per-entry FNV checksum @+0x1c. Fields: type@+4, seq@+8, cond/count@+0xc, node_id@+0x10, sizeA@+0x14, offB@+0x18, 0x30-byte payload@+0x20.rankedfs_mount replays the journal, maintaining a live directory of 16 inode slots (0x40 bytes each: rec[0:0x30]=name, f30@0x30, f34@0x34, nid@0x38, present@0x3c). Journal op types:
nid, copy 0x30-byte name payload, present=1inode[0x30:0x38] = entry[0x14:0x1c] (f30=block index, f34=size)present=0)root = copy(live); save marker = entry[0xc])entry[0xc] == saved marker: live = copy(root) (undo speculative changes since the snapshot)ctx = copy(live); committed_id = seq)This disk's journal contains type5/6 pairs with matching markers (101/101 @seq5/10, 202/202 @15/19, 303/303 @24/27), so seq 10/19/27 roll back the speculative writes (the speculative godmode nid99 / achievement nid98 inserts and a cleared.raid rename), leaving the committed directory = {player.dat(nid1), tutorial(nid2), placement(nid3), promotion(nid4), godmode(nid5), achievement(nid6)} with committed_id=28. This is literally "restore the committed timeline / only committed ticks affect MMR."
Environmental note: under this QEMU (cortex-a72, MMU OFF, SCTLR_EL1=0x00c50838 ⇒ all RAM is Device memory), mount always alignment-faults on an unaligned LDP inside sb_verify (ESR=0x96000021, DFSC=0x21). The firmware cannot actually complete mount in this emulator, so the entire solve was reproduced offline in Python by reimplementing the logic and cross-checking against the firmware via lldb over the QEMU gdbstub.
hashlib.blake2s(digest_size=32)). It was first mis-read as BLAKE2b because init stores four 64-bit words = eight 32-bit IV words with h[0] ^= 0x01010020.derive_key(name, ctx, scratch, flagbyte, salt16) = blake2s(name || ctx[0x404]=committed_id(4 LE) || flagbyte(1) || [if flagbyte>0: scratch[0:flagbyte*0x10]] || salt16).ctx[0x408] = blake2s("RANKEDFS-COMMITTED" || committed_id(4) || for each present committed inode: nid(4)+f30(4)+f34(4)+rec(0x30)).fs_read): per 32-byte block, ks = blake2s(ctx[0x408](32) || node.nid(4) || (byteoff>>5)(4) || "RANKEDFS-BLOCK"); plaintext = ciphertext ^ ks. File data lives at disk byte node.f30 * 0x1000, length node.f34./profile/player.dat with this key yields clean text PLAYER=SILVER WOLF / MODE=FINAL LV.999 / HIDDEN_MMR=998 / POLICY=COMMITTED_TICKS_ONLY, confirming the key and committed-directory reconstruction.verify_part1)verify reads exactly 48 bytes. verify_part1 runs a graph VM over the four committed replay files in order — tutorial(flagc0), placement(flagc1), promotion(flagc2), godmode(flagc3). A hidden-MMR seed starts at ctx[0x50]=0x3e3 (995); each file must add +1, reaching 0x3e7 (999) after all four.
Each file is a RAID//9 program: header (magic RAID//9, ver=9, nodes@0xa, edges, lanes@0x14, code-window offset f16@0x16, window length f18@0x18, section offsets), a node table (0x20-byte nodes: id, f2, op@4, f5, out_reg@8, in_reg@0xa, imm10@0x10, imm14@0x14, imm18@0x18), an edge list, and a lane-init table.
Opcode remap (16-byte table at sp+0xb0): for flagc==0 it is header[0x30:0x40] directly (a firmware quirk: 0x165c cbz flagc → 0x18b4 memcpy); for flagc>0 it is derive_key("RAID9-MAP", cid=28, flag=flagc, salt=header[0x40:0x50])[0:16] XOR header[0x30:0x40]. Nodes execute in deterministic topological order (ready-set, tie-break f2 desc, f5 desc, id asc). Selector = remap[node.op], must be ≤ 0xc.
Post-remap opcodes 0..12: op0 load u32 from code48[f16+imm10] (the ONLY input entry point), op1 imm, op2 ror/add mix, op3 ror/xor mix, op4 (imm10|1)*reg+imm14, op5 FNV/xxhash-style mul+xorshift, op6 two-register mix, op7/op12 swap, op8 RST (snapshot init_regs=regs, set flag), op9 CHK (xxHash over regfile with 0x9E3779B9/0x7f4a7c15/0x85ebca6b/0xaf286bcb, compared to node.imm14; COMMIT on match, ROLLBACK regs=init_regs & acc=chk_acc on mismatch), op10 acc mix (the +1), op11 set completion flag.
After the VM per file: require the op11 flag; then a TARGET-vector check — regfile must equal laneinit_bytes XOR blake2s-keystream(derive_key("RAID9-TARGET", flag=flagc, salt=header[0x68:0x78])). Then a DROP scratch update blake2s("RAID9-DROP" || flagc || regfile || acc || code48[f16:f16+f18]) fills scratch[flagc*0x10:...], integrity-checked against header[0x60:0x68]; scratch[0x30:0x50] is chained and scratch[0x50:0x54]=acc.
Coverage: tutorial reads code48 words 0-3 (bytes 0-15), placement words 2-7, promotion words 6-11, godmode all 12 — so the 48-byte code is pinned across all four files.
Parse both superblocks (verify FNV checksums), replay the JRNL journal in Python with the exact type1–type7 semantics above, honoring the type5/type6 snapshot/rollback pairs. This yields committed_id=28 and the committed directory of six inodes. Build ctx[0x408] (the file key) as BLAKE2s over "RANKEDFS-COMMITTED" + committed_id + each present inode's (nid,f30,f34,rec).
For each committed file, XOR each 32-byte block with blake2s(filekey || nid || (byteoff>>5) || "RANKEDFS-BLOCK"). Decrypting player.dat gives readable text (sanity check). Decrypt the four .raid replays and achievement.bin.
A faithful Python emulator (emu2.py) of verify_part1 + finalization was validated against firmware ground truth (tutorial's op9 checkpoint matched, acc 995→996, integrity check passed because header[0x60:0x68] stores blake2s(scratch[0:0x10]||header[0x40:0x50])[0:8]).
Solve per file symbolically, chaining scratch concretely between files. z3 with QF_BV was far too slow (44 modular multiplications; >30 min, OOM on bit-blast). Exporting to SMT-LIB2 and solving with bitwuzla — defining ext_rotate_left via bvshl/bvlshr/bvor because z3 emits a variable-rotate form bitwuzla doesn't know natively — solved each file in seconds.
Key correctness fix: op9 must be modeled as a symbolic If (commit vs rollback), NOT forced to match and NOT treated as a no-op. With naive "commit-always"/"no-op" op9 the constraints were UNSAT; the correct solution actually relies on the checkpoint branch. With the If-model, placement became SAT and produced ASCII text.
The recovered 48-byte player code is human-readable ASCII (a filler phrase ending in LV999!!!n0w!); the emulator confirms all four TARGET + integrity checks pass and the final seed reaches 999.
verify_part2 → ChaCha20-Poly1305 AEAD)verify_part2 reads /cache/achievement.bin (ACHV999 header: ver=1, outlen=0x40, magic32=0x20260999@0xc, ctlen=45@0x10, nonce=hdr[0x14:0x20], tag=hdr[0x20:0x30], ciphertext=hdr[0x40:0x6d]).
The ChaCha20 key is blake2s(code48[0:0x30] || scratch[0x30:0x50] || scratch[0x50:0x54] || scratch[0:0x30] || achv_magic32(4)). RFC 8439 ChaCha20-Poly1305 decryption: the Poly1305 tag verifies (definitive), and the plaintext is the flag.
#!/usr/bin/env python3 # High-level offline solve outline for GODMODE//999. # Full working scripts live in the task dir: emu2.py, gen_smt.py, solve_bw.py, # chacha_decrypt.py. This is the end-to-end skeleton. import struct from hashlib import blake2s DISK = open("ranked.img", "rb").read() def fnv1a(buf): h = 0x811c9dc5 for b in buf: h ^= b h = (h * 0x01000193) & 0xffffffff h ^= h >> 13 return h & 0xffffffff def b2s(*chunks): h = blake2s(digest_size=32) for c in chunks: h.update(c) return h.digest() # --- 1. Superblocks + committed journal replay ----------------------------- def parse_sb(off): sb = DISK[off:off+0x3c] assert sb[:4] == b"RNK9" committed_gen = struct.unpack_from("<I", sb, 0x14)[0] return committed_gen def replay_journal(): # 16 inode slots; apply type1..type7 with snapshot/rollback semantics. live = {} # nid -> dict(rec,f30,f34,present) root = None committed = {} committed_id = 0 off = 0x2000 while True: e = DISK[off:off+0x80] if e[:4] != b"JRNL": break typ = struct.unpack_from("<I", e, 4)[0] seq = struct.unpack_from("<I", e, 8)[0] cond = struct.unpack_from("<I", e, 0xc)[0] nid = struct.unpack_from("<I", e, 0x10)[0] a = struct.unpack_from("<I", e, 0x14)[0] b = struct.unpack_from("<I", e, 0x18)[0] pay = e[0x20:0x50] if typ == 1: live.setdefault(nid, {}) live[nid].update(rec=pay, present=1) elif typ == 2: live[nid]["f30"], live[nid]["f34"] = a, b elif typ == 3: live[nid]["rec"] = pay elif typ == 4: live[nid]["present"] = 0 elif typ == 5: replay_journal.marker = cond root = {k: dict(v) for k, v in live.items()} elif typ == 6: if cond == replay_journal.marker and root is not None: live = {k: dict(v) for k, v in root.items()} elif typ == 7: committed = {k: dict(v) for k, v in live.items()} committed_id = seq off += 0x80 return committed, committed_id # --- 2. File key + block decrypt ------------------------------------------- def file_key(committed, committed_id): parts = [b"RANKEDFS-COMMITTED", struct.pack("<I", committed_id)] for nid in sorted(n for n, v in committed.items() if v.get("present")): v = committed[nid] parts += [struct.pack("<III", nid, v["f30"], v["f34"]), v["rec"]] return b2s(*parts) def decrypt_file(fkey, nid, f30, f34): base = f30 * 0x1000 ct = DISK[base:base+f34] out = bytearray() for i in range(0, len(ct), 32): blk = ct[i:i+32] ks = b2s(fkey, struct.pack("<II", nid, i >> 5), b"RANKEDFS-BLOCK") out += bytes(x ^ y for x, y in zip(blk, ks)) return bytes(out) # --- 3. RAID//9 VM + bitwuzla solve -> code48 (see emu2.py/gen_smt.py) ------ # code48 recovered as printable ASCII; final MMR seed == 999. # code48 = <RECOVERED_48_BYTE_PLAYER_CODE> # do not embed here # --- 4. ChaCha20-Poly1305 flag decrypt (see chacha_decrypt.py) ------------- # key = b2s(code48[0:0x30], scratch[0x30:0x50], scratch[0x50:0x54], # scratch[0:0x30], achv_magic32) # flag = chacha20_poly1305_decrypt(key, nonce, ct, tag) # Poly1305 verifies
$ cat /etc/motd
Liked this one?
Pro unlocks every writeup, every flag, and API access. $9/mo.
$ cat pricing.md$ grep --similar