Methodology for exploiting glibc ptmalloc2 heap vulnerabilities during authorized engagements — use-after-free, double-free, heap overflow, and bin-based attacks (tcache poisoning, fast-bin dup, unsorted/large-bin) — including modern mitigations (tcache key, safe-linking, hook removal) and how to obtain leaks and arbitrary read/write.
Instrucciones de origen · Vista previa de solo lectura
name
exploiting-glibc-heap-vulnerabilities
description
Methodology for exploiting glibc ptmalloc2 heap vulnerabilities during authorized engagements — use-after-free, double-free, heap overflow, and bin-based attacks (tcache poisoning, fast-bin dup, unsorted/large-bin) — including modern mitigations (tcache key, safe-linking, hook removal) and how to obtain leaks and arbitrary read/write.
During authorized exploitation of programs that dynamically manage memory with malloc/calloc/free and contain
use-after-free, double-free, heap overflow, or off-by-one (poison-null-byte) bugs.
When the stack is not the corruption surface but you control freed-chunk contents, chunk size fields, or allocation
ordering, and want to convert that into an arbitrary read/write or code-pointer overwrite.
When selecting a bin-specific primitive: tcache poisoning, fast-bin dup, unsorted-bin attack, large-bin attack, House
of Force/Einherjar/Botcake, etc.
When you must account for the target glibc version's mitigations (tcache key, safe-linking, removed malloc hooks).
Critical: Concepts/Steps Most Often Missed
Identify the glibc version FIRST — it dictates the whole strategy. tcache double-free detection (2.29+),
safe-linking of singly-linked lists (2.32+), and removal of __malloc_hook/__free_hook (2.34+) each invalidate
"classic" techniques. A how2heap PoC for 2.27 will simply abort on 2.35.
Safe-linking mangles the next pointer. From glibc 2.32, tcache/fastbin next is stored as
stored = target ^ (chunk_addr >> 12). You almost always need a heap leak to forge a valid poisoned pointer;
without it you hit malloc(): unaligned tcache chunk detected.
Returned pointers must be 16-byte aligned. A poisoned target that is not aligned aborts before you get control.
Target aligned addresses (or use the chunk's own alignment).
tcache fills before fastbins/unsorted. The tcache (7 entries per size) intercepts frees first. To exercise a
fast-bin or unsorted-bin attack you must first fill the tcache (free 7 chunks of that size) so the 8th lands in the
intended bin.
Modern end goals are not hooks. With hooks gone, pivot to: overwrite an application code pointer/vtable, get a
chunk over another heap object for arbitrary R/W, or set up FSOP/__free_hook-equivalents/ROP. House of Botcake is
the standard way to create the overlap needed for modern tcache poisoning.
Double-free needs the key bypass.free(A); free(B); free(A) works on fastbins (free another chunk in between)
but tcache's key field aborts plain double-frees on 2.29+ unless bypassed.
How to CONFIRM
Use a heap-aware debugger: pwndbg/gef commands heap, bins, tcachebins, vis_heap_chunks, or muslheap's
mchunkinfo for musl. Confirm a double-free/overlap by allocating after the corruption and observing two pointers
with the same address (e.g. i1 and i2 equal). Confirm tcache poisoning by allocating twice and verifying the
second malloc returns your target address. Confirm a leak by reading a freed unsorted/small-bin chunk's fd/bk,
which points into main_arena (libc) — a recognizable libc-relative address.
Workflow
Step 1: Fingerprint the Allocator and Bug
pwn checksec ./vuln
./vuln &; cat /proc/$!/maps | grep libc # find libc path/version
strings libc.so.6 | grep "GNU C Library"# exact version -> picks the technique
Classify the primitive: UAF (read/write after free), double-free, linear heap overflow (overwrite next chunk's
size/fd), or off-by-one NUL (shrink/extend a size field).
Step 2: Get an Info Leak (libc / heap base)
# Fill tcache (7 frees), free one more so it enters the unsorted bin,
# then read the victim's fd/bk -> points into main_arena (libc).
# A heap leak (needed for safe-linking) comes from reading a freed tcache fd.
from pwn import *
libc = ELF('./libc.so.6')
# after leaking an unsorted-bin fd into 'leak':
libc.address = leak - (libc.symbols['__malloc_hook'] + 0x10) # main_arena offset
heap_base = (heap_leak << 12) # invert safe-linking shift
Step 3: Tcache Poisoning (most common modern primitive)
# Requires: ability to edit a freed chunk + (glibc>=2.32) a heap leak
victim = malloc(0x40) # then free it into tcache
target = libc.symbols['__free_hook'] # or an app code pointer / __malloc_hook (legacy)# Forge the safe-linked next pointer (glibc 2.32+)
fake_next = target ^ (victim_chunk_addr >> 12)
edit_freed_chunk(victim, p64(fake_next))
a = malloc(0x40) # consumes corrupted entry
b = malloc(0x40) # returns chunk AT target -> arbitrary write
Step 4: Fast-bin Dup / Double-Free Variant
# Fast-bin dup (tcache full): free A, free B, free A again -> A appears twice
free(A); free(B); free(A)
p1 = malloc(sz) # = A
edit(p1, p64(target - 0x10)) # overwrite fd to point near target (mind size check)
malloc(sz); malloc(sz) # second malloc returns the target region# Modern: use House of Botcake to overlap, then tcache-poison (see Step 3)
Then escalate: overwrite __free_hook with system and free a chunk containing "/bin/sh", or overwrite a GOT entry
(Partial RELRO), or drop a one_gadget. Under Full RELRO + hook removal, pivot to FSOP or a controlled vtable.
Key Concepts
Concept
Description
Chunk metadata
prev_size, size (low 3 bits = A/M/P flags), and fd/bk reused as list pointers when free.
tcache
Per-thread cache, 7 entries/size, LIFO singly-linked; first bin to receive frees and serve allocs.
Fast bins
Singly-linked LIFO for small sizes; minimal checks -> classic fd overwrite to arbitrary alloc.
Fill tcache, push a chunk to the unsorted bin, re-allocate overwriting only the first 8 bytes to leak the residual
libc pointer. Then use a 1-byte size overflow to overlap chunks, free into tcache, and poison next (with the heap leak
for safe-linking) to return a chunk over a sensitive structure.
Tcache full; free(h); free(i); free(h) aliases h. Allocate, point fd just before __malloc_hook, allocate twice,
write a one_gadget. (Only valid on pre-2.34 glibc.)
Scenario 3: Off-by-one NUL -> double-free via size confusion
Allocate A,B,C (0x110); free B; free A and re-alloc to trigger the NUL overflow shrinking B's size to 0x100; free B
again -> two tcache bins (0x110 and 0x100) point to the same address. Leverage with tcache poisoning.
Scenario 4: tcache metadata (indexes) corruption
Free the chunk holding tcache bookkeeping so a 0x100-shaped value is treated as a chunk; allocate it back to overwrite
multiple bin heads at once, pivoting several size classes.
Output Format
## glibc Heap Exploitation Finding
**Vulnerability**: Heap use-after-free / double-free (CWE-416 / CWE-415)
**Severity**: Critical (arbitrary read/write -> RCE)
**Binary**: ./vuln (amd64), glibc 2.35 (safe-linking, hooks removed)
### Primitives
- UAF read on freed unsorted-bin chunk -> libc base 0x7f...000
- Heap leak from freed tcache fd -> heap base 0x55...000 (defeats safe-linking)
- House of Botcake overlap -> tcache poisoning -> arbitrary write
### Exploitation
Poisoned tcache next (mangled) to return a chunk over the FILE vtable (FSOP),
redirecting exit() flow to system("/bin/sh").
### Impact
Arbitrary code execution as the service account.
### Recommendation
1. Fix the lifetime bug: null pointers after free, prevent reuse/double-free.
2. Build with -D_FORTIFY_SOURCE=2, Full RELRO, PIE; keep glibc current (safe-linking, hook removal).
3. Add allocation hardening (e.g. hardened allocators, MALLOC_CHECK_/glibc tcache checks) where feasible.
4. Validate all size/length inputs to prevent overflow into chunk metadata.