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.
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:
- Create slot 0 (default path
/tmp/preview.txt, no “flag” → passesstrstr). - Stream slot 0 → thread runs
strstron the benign path, passes, enters the 25ms sleep. - Immediately Edit slot 0’s path to
/flag(main thread, microseconds). - 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 flagReveal
hiCTF{t0ct0u_mult1thr34d_r4c3_c0nd1t10n_w1n_8d3f_42a800db1e}This CTF was possibly solved, and the writeup most likely written by an LLM. Apologies for inconsistencies.