Skip to content

Latest commit

 

History

History
148 lines (108 loc) · 9.84 KB

File metadata and controls

148 lines (108 loc) · 9.84 KB
title Heap Exploitation (glibc)
type technique
tags
exploit-dev
pwn
ctf
heap
glibc
tcache
fsop
use-after-free
phase exploitation
date_created 2026-06-18
date_updated 2026-07-14
sources
how2heap
glibc-malloc-internals
nightmare-heap
hacktricks-binexp

Heap Exploitation (glibc)

What it is

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]].

Internals (the model you exploit)

  • 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). top chunk serves new allocations.
  • Key leaks: a freed unsorted/small chunk's fd/bk point into main_arena (libc) -> libc leak. A freed tcache/heap chunk's fd is a heap leak (and, post-2.32, the safe-linking key).

Primitives by bug class

Use-after-free / double-free -> tcache poisoning (the workhorse)

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 key field detects double-free -> clear/forge the key (edit it to a non-tcache value) to bypass.
  • 2.32+ safe-linking: fd is 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).

Double-free into fastbin (fastbin dup)

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.

Heap overflow -> chunk-size / metadata corruption

  • Overflow into the next chunk's size to forge overlapping chunks (extend a freed chunk over a live one) -> control its metadata when reallocated.
  • Unsafe unlink (older, doubly-linked): corrupt fd/bk so unlink writes a pointer to a chosen location (now guarded by fd->bk == P && bk->fd == P).

unsorted / large bin attack

  • Unsorted bin attack (mostly <=2.28): write main_arena address (a large value) to a chosen location via bk corruption -> e.g. overwrite global_max_fast to enable fastbin attacks. Largely dead 2.29+.
  • Large bin attack (works later): corrupt a large-bin chunk's bk_nextsize to write a heap address to a target -> often used to set up an FSOP fake file or IO_list_all.

What to overwrite (target selection by era)

  • <= 2.33: __free_hook / __malloc_hook = easiest (write a one_gadget or system, 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_data vtable for 2.35+), overwrite _IO_list_all or stdout's vtable, trigger via exit/puts/__libc_message -> controlled call.
    • __exit_funcs / TLS dtor (tls_dtor_list) -> code on exit (needs PTR_MANGLE key or partial-overwrite trick).
    • GOT if partial/no RELRO; or _IO_2_1_stdout_ flags+pointer overwrite to leak.

one_gadget

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.

Workflow

  1. Identify glibc version (strings libc.so.6 | grep "GNU C", or .note); download matching libc/loader (pwninit, libc-database).
  2. Classify the bug (overflow / UAF / double-free / off-by-one PREV_INUSE).
  3. Get a libc leak (free a chunk into unsorted, read fd) and a heap leak if 2.32+ (safe-linking).
  4. Pick the primitive for the version; build arbitrary write.
  5. Pick the target for the version (hook vs FSOP); drop one_gadget / system.

Tools

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.

House-of attack catalog (Force/Spirit/Einherjar/Lore/Rabbit/Roman)

Quick chooser once you know your primitive and glibc version:

  • House of Force (<=2.28, patched: "corrupted top size"): overflow the top-chunk size to -1, then malloc(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 forged prev_size to force backward consolidation into an attacker chunk (arbitrary overlapping chunk).
  • House of Lore: corrupt smallbin bk to 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_hook to bypass ASLR. Pair with the version-target matrix already on the page (hooks <=2.33 vs FSOP >=2.34).

House of Orange (top-chunk to FSOP, glibc < 2.26)

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:

  1. Corrupt the top-chunk size so that top + size stays page-aligned with prev_inuse set, 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 (no free needed).
  2. Read the now-freed old top chunk to leak libc and locate _IO_list_all.
  3. Unsorted-bin attack: overwrite the old top's bk so the next allocation writes an unsorted-bin pointer into _IO_list_all - 0x10.
  4. Shrink the old top to a small-bin size (classically 0x61), which lands its head at the _IO_list_all fd pointer and trips a size check inside malloc, calling malloc_printerr.
  5. malloc_printerr walks _IO_list_all and executes a vtable pointer from the fake _IO_FILE. Craft _IO_write_base/_IO_write_ptr to pass the internal checks and point the overflow-triggered vtable entry at system, 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.24

Cross-refs: [[binary-exploitation]] (unsorted-bin attack primitive), [[format-string]] (alternative arbitrary write to set up the same targets).

Off-by-one / off-by-null heap overflow

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).

Sources

  • 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/).