Skip to content

Heap Exploitation

The heap is the dynamic memory managed by malloc/free. Unlike the stack, there are no return addresses to overwrite, but the memory manager (the allocator) stores metadata between and around chunks, and that metadata can be corrupted. Heap exploitation consists of manipulating that metadata —via overflows, use-after-free, double-free— to trick the allocator into arbitrary writes that end in control of the flow. It’s more complex than the stack and very dependent on the libc version.

# a memory chunk has a header with metadata:
[ prev_size ][ size | flags ][ ... user data ... ]
# on free, free chunks are stored in linked lists (bins):
tcache (glibc >=2.26) fast per-size lists (the modern target)
fastbins LIFO lists for small chunks
unsorted/small/large bins
# the fd/bk pointers of those lists live in the freed chunk's data

The key: when a chunk is freed, the allocator writes list pointers into its data area. If you control those pointers, you control where the allocator “thinks” free chunks are → malloc returns a pointer wherever you want.

Heap overflow write past a chunk -> corrupt the next chunk's header
Use-After-Free (UAF) use a chunk after free -> read/write another's data, or bin pointers
Double-Free free twice -> corrupt the lists (tcache/fastbin)
Type confusion treat an object as another type
tcache poisoning (modern, the most common) corrupt a chunk's fd in tcache
-> the next malloc returns a controlled address
fastbin dup fastbin double-free -> return the same chunk twice
unlink (old) abuse the unlink macro -> arbitrary write
House of <X> family of advanced techniques (Spirit, Force, Einherjar, Orange...)
for scenarios with limited primitives
# with a UAF or double-free over tcache:
1. free(A) # A goes to tcache; its fd points to the next free
2. edit A->fd = target_address # corrupt the pointer (UAF/overflow)
3. malloc() -> returns A
4. malloc() -> returns target_address # write/read wherever you want!
# typical target: __free_hook / __malloc_hook (old libc) or the GOT -> system
# with a heap write-what-where:
# old libc: overwrite __free_hook = system, then free(chunk with "/bin/sh")
# modern libc (no hooks): FSOP (File Stream Oriented Programming),
# overwrite _IO_FILE structures, or point to one_gadget
pwntools interaction and packing
gdb + pwndbg/GEF heap commands: 'heap', 'bins', 'tcache' -> see the allocator state
glibc source / how2heap reference of techniques by version
# how2heap (shellphish): reproducible examples of EVERY technique

Heap exploitation changes a lot between glibc versions (mitigations are added: tcache key, safe-linking from 2.32, etc.). Always identify the exact libc version and use the applicable technique:

# safe-linking (glibc >=2.32): tcache/fastbin fd pointers are "encrypted" with the address
# -> you need a heap leak to compute the correct value
  • Updated glibc (heap mitigations: tcache key against double-free, safe-linking).
  • Hardened allocators (e.g. hardened_malloc) in critical environments.
  • Sanitizers (ASan, with UAF/heap-overflow detection) in development and fuzzing.
  • Memory-safe languages (Rust) eliminate UAF/double-free by design.
  • Code review of pointer/size handling; don’t use pointers after free (set to NULL).
  • Identify the exact glibc version
  • Identify the bug: overflow / UAF / double-free
  • See the heap state in gdb (heap/bins/tcache)
  • Heap leak if safe-linking (>=2.32)
  • tcache/fastbin poisoning → malloc returns a controlled address
  • Write-what-where → __free_hook/GOT/FSOP → system/one_gadget
  • Shell/flag locally → remote
  • Practice the specific technique with how2heap