| title | Heap Exploitation (glibc) | ||||||||
|---|---|---|---|---|---|---|---|---|---|
| type | technique | ||||||||
| tags |
|
||||||||
| phase | exploitation | ||||||||
| date_created | 2026-06-18 | ||||||||
| date_updated | 2026-07-14 | ||||||||
| sources |
|
The dominant modern CTF pwn category and the basis of most real-world allocator bugs. Exploits glibc malloc/free metadata (chunk headers, free-list bins) to turn a heap overflow, use-after-free, or double-free into an arbitrary read/write and then code execution. Which technique applies depends on the glibc version (mitigations were added incrementally). Stack-based pwn: [[binary-exploitation]]; finding the bug: [[memory-safety-bugs]], [[fuzzing]]; analysis: [[reverse-engineering]].
- Chunk:
prev_size | size(+flags A,M,P) | fd | bk | ... | data. Size includes the 8/16-byte header; low 3 bits are flags (PREV_INUSE). - Bins:
tcache(per-thread, 7 chunks/size, LIFO, glibc 2.26+),fastbins(small, LIFO, single-linked),unsorted,small,large(double-linked).topchunk serves new allocations. - Key leaks: a freed unsorted/small chunk's
fd/bkpoint intomain_arena(libc) -> libc leak. A freed tcache/heap chunk'sfdis a heap leak (and, post-2.32, the safe-linking key).
Free a chunk, keep a dangling pointer (UAF) or free twice (double-free), then overwrite the freed chunk's fd so the next two allocations return an attacker-chosen address -> arbitrary allocation -> arbitrary write.
free(a); // a -> tcache
edit(a, target_addr); // overwrite fd (UAF)
b = malloc(sz); // returns a
c = malloc(sz); // returns target_addr -> write anywhere
- 2.29+: tcache
keyfield detects double-free -> clear/forge the key (editit to a non-tcache value) to bypass. - 2.32+ safe-linking:
fdis stored as(chunk_addr >> 12) ^ next_ptr. You need a heap leak to forge:mangled = (addr_of_fd >> 12) ^ target. Also alignment-checked (target must be 16-aligned).
When tcache is full/unavailable, double-free into a fastbin. Requires the forged target to hold a valid size field matching the fastbin index (the "fake chunk size" requirement). Use when chunk size > tcache range or tcache exhausted.
- Overflow into the next chunk's
sizeto forge overlapping chunks (extend a freed chunk over a live one) -> control its metadata when reallocated. - Unsafe unlink (older, doubly-linked): corrupt
fd/bksounlinkwrites a pointer to a chosen location (now guarded byfd->bk == P && bk->fd == P).
- Unsorted bin attack (mostly <=2.28): write
main_arenaaddress (a large value) to a chosen location viabkcorruption -> e.g. overwriteglobal_max_fastto enable fastbin attacks. Largely dead 2.29+. - Large bin attack (works later): corrupt a large-bin chunk's
bk_nextsizeto write a heap address to a target -> often used to set up an FSOP fake file or IO_list_all.
- <= 2.33:
__free_hook/__malloc_hook= easiest (write a one_gadget orsystem, trigger via free/malloc). - >= 2.34: hooks removed. Pivot to one of:
- FSOP / _IO_FILE (House of Orange, House of Apple 2): forge an
_IO_FILE(and_IO_wide_datavtable for 2.35+), overwrite_IO_list_allor stdout's vtable, trigger viaexit/puts/__libc_message-> controlled call. __exit_funcs/ TLS dtor (tls_dtor_list) -> code on exit (needsPTR_MANGLEkey or partial-overwrite trick).- GOT if partial/no RELRO; or
_IO_2_1_stdout_flags+pointer overwrite to leak.
- FSOP / _IO_FILE (House of Orange, House of Apple 2): forge an
one_gadget ./libc.so.6 finds single-jump execve("/bin/sh") addresses; each has register/stack constraints - pick one whose constraints hold at the call site, else use system("/bin/sh") with a controlled rdi.
- Identify glibc version (
strings libc.so.6 | grep "GNU C", or.note); download matching libc/loader (pwninit, libc-database). - Classify the bug (overflow / UAF / double-free / off-by-one
PREV_INUSE). - Get a libc leak (free a chunk into unsorted, read
fd) and a heap leak if 2.32+ (safe-linking). - Pick the primitive for the version; build arbitrary write.
- Pick the target for the version (hook vs FSOP); drop one_gadget /
system.
pwndbg/gef (heap, bins, vis, tcachebins), pwntools, pwninit, one_gadget, libc-database/seccomp-tools (for seccomp jails), how2heap (shirou/Nightmare) as the canonical technique reference.
Quick chooser once you know your primitive and glibc version:
- House of Force (<=2.28, patched: "corrupted top size"): overflow the top-chunk
sizeto -1, thenmalloc(target - old_top - 4*sizeof(long))walks top to any address; next malloc returns it. Needs top-size overwrite + attacker-controlled malloc size. - House of Spirit: free a fake fast/tcache chunk you crafted (valid
size, and on tcache 16-aligned + a valid bin) so a later malloc hands you a stack/BSS address. tcache path skips the next-size check so one fake chunk suffices; safe-linking is auto-applied on free so no key needed for a single chunk. Great post-2.34 (no hooks) to overlap a target buffer.unsigned long long fake[6] __attribute__((aligned(0x10))); fake[1] = 0x41; // size, prev_inuse set free(&fake[2]); void *q = malloc(0x30); // q == &fake[2]
- House of Einherjar: uses an off-by-null on
prev_inuse+ a forgedprev_sizeto force backward consolidation into an attacker chunk (arbitrary overlapping chunk). - House of Lore: corrupt smallbin
bkto return a fake chunk from a smallbin allocation. - House of Rabbit: fastbin corruption via
realloc-driven size changes. - House of Roman: leakless, uses partial overwrites + fastbin/
__malloc_hookto bypass ASLR. Pair with the version-target matrix already on the page (hooks <=2.33 vs FSOP >=2.34).
The only house that reaches code execution without ever calling free. It turns a single heap overflow plus a libc leak into arbitrary execution by forging a fake _IO_FILE and abusing the file-stream cleanup that malloc_printerr used to perform. Fixed in glibc 2.26 (the abort path no longer walks _IO_list_all), so it applies to older targets and CTF pins.
Requirements: overwrite the top-chunk size, plus a libc and heap leak.
Chain:
- Corrupt the top-chunk size so that
top + sizestays page-aligned withprev_inuseset, then request a chunk larger than the forged top size. glibc cannot serve it from the top, so the old top chunk is pushed into the unsorted bin (nofreeneeded). - Read the now-freed old top chunk to leak libc and locate
_IO_list_all. - Unsorted-bin attack: overwrite the old top's
bkso the next allocation writes an unsorted-bin pointer into_IO_list_all - 0x10. - Shrink the old top to a small-bin size (classically
0x61), which lands its head at the_IO_list_allfd pointer and trips a size check insidemalloc, callingmalloc_printerr. malloc_printerrwalks_IO_list_alland executes a vtable pointer from the fake_IO_FILE. Craft_IO_write_base/_IO_write_ptrto pass the internal checks and point the overflow-triggered vtable entry atsystem, with"/bin/sh"at the head of the struct.
On glibc >= 2.24 the _IO_FILE vtable is verified against the __libc_IO_vtables range, so modern variants pivot through _IO_str_jumps/_IO_str_overflow (the "FSOP" family) or use _IO_wfile_jumps rather than a raw vtable pointer. House of Orange is the gateway to that broader _IO_FILE/FSOP (File Stream Oriented Programming) class: once you can forge a file struct and reach _IO_flush_all_lockp (called on exit, on abort, or when stdio buffers flush), the same fake-struct trick gives execution on current glibc.
# reference implementation and per-version variants
# how2heap: glibc_2.23/house_of_orange.c ; later FSOP: _IO_str / _IO_wstr variants
# inspect the file-struct and vtable range while building the fake struct
pwndbg> p _IO_list_all
pwndbg> p &_IO_2_1_stdout_
pwndbg> p __libc_IO_vtables # allowed vtable range on glibc >= 2.24Cross-refs: [[binary-exploitation]] (unsorted-bin attack primitive), [[format-string]] (alternative arbitrary write to set up the same targets).
A 1-byte overflow into the next chunk's size (arbitrary byte) or a NUL terminator writing 0x00
onto it (off-by-null, e.g. strlen/strcpy mismatch) forges overlapping chunks. General
arbitrary-byte flow: alloc A,B,C (+ guard vs top); free C; overflow A into B to grow B's size so
B now covers freed C; free B, realloc over it, then edit C's fd (tcache poisoning) for
arbitrary write. Off-by-null flow: shrink the middle chunk's advertised size with a 0x00 so a
later free consolidates across a still-live inner chunk, reclaim it to control its contents.
def protect(ptr, chunk): return ptr ^ (chunk >> 12) # safe-linking >=2.32
def reveal(enc, chunk): return enc ^ (chunk >> 12)Modern (>=2.32): needs a heap leak to recompute the safe-linking XOR before poisoning; from 2.28
prev_size vs size consistency check complicates classic shrink. Real target: glibc
__vsyslog_internal off-by-one (CVE-2023-6779).
- shellphish how2heap (slug: how2heap) (
https://github.com/shellphish/how2heap). - glibc malloc internals / sourceware (slug: glibc-malloc-internals).
- Nightmare CTF course - heap chapters (slug: nightmare-heap) (
https://guyinatuxedo.github.io/).