Neven Marić
Hidden Investigations CTF 2026

PwnHidden Investigations CTF 2026

Apex Engine

A menu-driven custody vault with two TOCTOU races: burst 8 concurrent escrow transfers to underflow an unsigned balance, then swap a benign file path for /flag during a 25 ms sleep window to bypass the clearance guard.

  • pwn
  • race-condition
  • toctou
  • multithreading
  • integer-underflow
  • pwntools

What this one teaches

  • Recognising check-then-sleep-then-act as a TOCTOU race even when there is no memory corruption — pure logic bugs in multithreaded code.
  • Exploiting unsigned integer underflow by racing multiple concurrent debits past a single balance check.
  • Pipelining two commands (stream + edit) into one TCP burst to reliably land inside a 25 ms race window over the network.

Task description

Welcome to the Apex Multi-Threaded Settlement Engine — a high-frequency asset custody vault built for low-latency asynchronous transaction processing. Our internal risk model asserts that funds cannot be double-spent, and classified vault records are completely shielded from non-root clearance. Can you identify the flaws and recover the flag?

  • Category: pwn (59 pts)
  • Challenge ID: 596582e4-cc87-4c51-9e76-8f4d2838af6b
  • Binary: ELF 64-bit PIE, stripped. Mitigations: Full RELRO, NX, PIE, BIND_NOW, stack canary. No memory-corruption primitive — this is a pure logic / race-condition challenge.

Overview

“Apex Multi-Threaded Settlement Engine” is a menu-driven custody vault. The author claims two invariants: funds cannot be double-spent, and classified vault records are shielded from non-root clearance. Both are false, each broken by a TOCTOU race because worker threads check-then-sleep-then-act with no locking.

Key globals (BSS):

addr meaning init
0x43e0 cash balance 100
0x43e8 escrow vault 0
0x43f0 VIP tier flag 0
0x43f4 root clearance 0 — only ever read, never written

Main menu: 1 View, 2 Initiate Escrow Transfer, 3 Withdraw from Escrow, 4 Upgrade to VIP (costs 10,000), 5 Access Secure Vault Enclave (requires VIP), 6 Exit. The flag lives at /flag (fallback ./flag.txt) and can only be read from inside the enclave’s streaming thread.

Bug 1 — double-spend race (settlement worker @0x1ef0)

Each “Initiate Escrow Transfer” (opt 2) spawns a detached pthread running:

if (order->status != 1) return;
cash = GLOBAL_CASH;              // (1) read + check
if (cash < order->amount) { ... return; }
order->status = 2;              // PROCESSING
usleep(25000);                  // (2) 25ms window, no lock held
GLOBAL_CASH  -= order->amount;  // (3) debit — after the sleep
GLOBAL_ESCROW += order->amount;

The main-thread dispatcher never debits cash; only the worker does, and only after a 25ms sleep that follows the check. Firing 8 transfers of 100 in a single burst (all 8 land in the socket buffer at once, so all workers spawn within microseconds) makes every worker read cash = 100, pass the check, then debit. After 8 debits the unsigned balance underflows:

cash = 100 - 8*100  (mod 2^64)  = 18446744073709550916

That astronomically-large balance pays for the VIP upgrade. (Do not then “Withdraw from Escrow” — cash += escrow would wrap the value back down to 100.)

Bug 2 — clearance / path TOCTOU (enclave stream thread @0x1d80)

Once VIP, the enclave offers: Create Slot, Stream Document, Edit Target Path. The stream thread does:

if (strstr(path, "flag") && GLOBAL_CLEARANCE == 0) { print "SECURITY ALERT ... denied"; return; }
usleep(25000);                  // 25ms window
fp = fopen(path, "r");          // path re-read from the slot buffer
... fwrite(contents) ...

The substring guard (strstr(path,"flag")) is evaluated once at thread start, then the path buffer is read again by fopen 25ms later — and Edit Target Path writes that same buffer in place from the main thread. Since root clearance (0x43f4) is never set, any “flag” path is permanently blocked through the guard — but the guard can be bypassed by editing the path after the check:

  1. Create slot 0 (default path /tmp/preview.txt, no “flag” → passes strstr).
  2. Stream slot 0 → thread runs strstr on the benign path, passes, enters the 25ms sleep.
  3. Immediately Edit slot 0’s path to /flag (main thread, microseconds).
  4. Thread wakes and fopen("/flag") → flag streamed back.

The two sub-races must both hold: the edit must land after the thread’s strstr (else it sees “/flag” and denies) but before the fopen (else it reads the preview). Sending stream + edit as one pipelined burst satisfies this on the server side; a small retry loop makes it reliable against scheduling jitter over the network.

Exploit chain

race-inflate cash (8× escrow transfer, burst) → upgrade to VIP → enter enclave → create slot → (stream + edit→/flag) burst → read flag.

The exploit (pwntools) connects over TLS, fires the burst transfers, upgrades to VIP, enters the enclave, and pipelines the stream+edit race.

Output:

[*] cash balance after race = 18446744073709550916
[+] VIP tier acquired
================ [SECURE ENCLAVE DATA STREAM] ================
hiCTF{t0ct0u_mult1thr34d_r4c3_c0nd1t10n_w1n_8d3f_42a800db1e}

Submitted to the platform API → {"kind":"goodFlag","message":"The flag is correct."}.

Root cause / fix

Both bugs are check-then-act across a sleep with no synchronization. Correct design would hold a mutex across the balance check+debit and across the path validation+open (or canonicalize + re-validate the path immediately before fopen), and the “root clearance” gate should fail closed rather than depend on a flag that is never set.

The flagRevealhiCTF{t0ct0u_mult1thr34d_r4c3_c0nd1t10n_w1n_8d3f_42a800db1e}

This CTF was possibly solved, and the writeup most likely written by an LLM. Apologies for inconsistencies.