Heap Overflow & Use-After-Free

Overview

Heap exploitation targets dynamically allocated memory. Unlike stack exploits that overwrite return addresses, heap exploits corrupt allocator metadata or application data stored on the heap. The goal is typically to achieve an arbitrary write primitive — writing a controlled value to a controlled address — which is then leveraged for code execution.

This file focuses on glibc ptmalloc2, the default allocator on Linux. Heap exploitation is version-dependent — techniques that work on one glibc version may fail on another due to added security checks.

ATT&CK Mapping

  • Tactic: TA0002 - Execution
  • Technique: T1203 - Exploitation for Client Execution

Prerequisites

  • A binary with a heap vulnerability (overflow, use-after-free, double-free)
  • Understanding of the target glibc version and its heap implementation
  • GDB with pwndbg (provides heap, bins, vis commands for heap inspection)
  • pwntools for exploit scripting

glibc Heap Basics

Chunk Structure

Every malloc() allocation returns a pointer to user data within a larger "chunk" structure. The chunk header sits immediately before the user data:

┌──────────────────────────────┐
│    prev_size (8 bytes)       │  ← only used if previous chunk is free
├──────────────────────────────┤
│    size (8 bytes)            │  ← chunk size + flags (low 3 bits)
├──────────────────────────────┤  ← pointer returned by malloc()
│    user data                 │
│    ...                       │
├──────────────────────────────┤
│    next chunk header         │
└──────────────────────────────┘

The size field contains the chunk size with three flag bits: - Bit 0 (P) — PREV_INUSE: previous chunk is allocated (not free) - Bit 1 (M) — IS_MMAPPED: chunk was allocated with mmap() - Bit 2 (A) — NON_MAIN_ARENA: chunk belongs to a thread arena

Free Chunk Structure

When a chunk is freed, its user data area is repurposed for freelist pointers:

┌──────────────────────────────┐
│    prev_size                 │
├──────────────────────────────┤
│    size                      │
├──────────────────────────────┤
│    fd (forward pointer)      │  ← points to next free chunk
├──────────────────────────────┤
│    bk (backward pointer)     │  ← points to previous free chunk
├──────────────────────────────┤
│    (remaining user data)     │
└──────────────────────────────┘

Bin Types

glibc maintains several "bins" — linked lists of free chunks organized by size:

Bin Type Size Range (x86-64) Structure
tcache Up to 0x410 bytes Per-thread singly linked list (7 entries per size)
fastbin Up to 0x80 bytes Singly linked list (LIFO)
unsorted bin Any size Doubly linked list (temporary holding)
small bins Less than 0x400 bytes Doubly linked list (62 bins, exact size)
large bins 0x400+ bytes Doubly linked list (sorted by size)

Freed chunks go to tcache first (if available), then fastbins or unsorted bin.

Inspecting the Heap in GDB

# pwndbg
# https://github.com/pwndbg/pwndbg
# Inside GDB with pwndbg:

# Show all heap chunks
pwndbg > heap

# Show all bins (free lists)
pwndbg > bins

# Show tcache entries
pwndbg > tcachebins

# Show fastbin entries
pwndbg > fastbins

# Visualize heap layout
pwndbg > vis_heap_chunks

Heap Overflow

A heap overflow occurs when writing beyond the bounds of a heap allocation, corrupting the header or data of the adjacent chunk.

Overwriting Application Data

If two allocations are adjacent, overflowing the first can corrupt data in the second:

char *a = malloc(32);
char *b = malloc(32);     // b is adjacent to a on the heap
strcpy(a, long_input);    // overflows into b's chunk

This can overwrite: - Function pointers stored in b - Object metadata (vtable pointers in C++) - File names, command strings, or other data the program uses

Overwriting Chunk Metadata

Overwriting the size field of the next chunk can cause the allocator to misinterpret chunk boundaries, leading to overlapping allocations.

Use-After-Free (UAF)

A use-after-free occurs when a program continues to use a pointer after the memory it points to has been freed. If the freed memory is reallocated for a different purpose, the dangling pointer now accesses the new data.

char *ptr = malloc(64);
free(ptr);                  // ptr is now dangling

char *new_ptr = malloc(64); // may reuse the same memory
// ptr and new_ptr may point to the same address
strcpy(ptr, "controlled");  // corrupts new_ptr's data

Exploitation Pattern

  1. Allocate object A
  2. Free object A (goes to tcache or fastbin)
  3. Allocate object B of the same size (gets the same memory as A)
  4. Use the dangling pointer to A — it now reads/writes object B's data
  5. If object B contains function pointers, overwrite them for code execution

Double-Free

Freeing the same chunk twice corrupts the freelist. When the same chunk appears twice in the free list, two subsequent allocations return the same pointer, creating an overlap.

char *a = malloc(64);
free(a);
free(a);    // double-free — same chunk in freelist twice

char *b = malloc(64);    // gets a's chunk
char *c = malloc(64);    // also gets a's chunk — b == c

glibc Double-Free Protections

Modern glibc versions detect double-frees:

  • tcache key field (glibc 2.29+): freed tcache chunks store a key value; free() checks for it before inserting. Bypass: overwrite the key before the second free()
  • fastbin check: detects consecutive frees of the same chunk. Classic bypass: free A, free B, free A (interleave to avoid detection)

Tcache Poisoning

Tcache (thread-local cache, glibc 2.26+) uses a singly linked list. Corrupting the fd pointer of a free tcache chunk causes malloc() to return an attacker-controlled address.

Attack Steps

  1. Free chunk A — it goes into tcache
  2. Overwrite A's fd pointer (via overflow, UAF, or double-free)
  3. Set fd to target address (e.g., __malloc_hook or a stack address)
  4. malloc() returns chunk A
  5. malloc() again — returns the target address
  6. Write to the returned pointer to control the target location

Safe-Linking (glibc 2.32+)

Starting with glibc 2.32, tcache and fastbin fd pointers are obfuscated using safe-linking. The stored fd is XORed with the chunk's address shifted right by 12 bits:

stored_fd = real_fd ^ (chunk_addr >> 12)

To poison the fd, you need to know (or leak) the chunk address to compute the correct obfuscated value.

Common Exploitation Targets

Target Glibc Version Notes
__malloc_hook < 2.34 Called on every malloc(), one-shot to shell
__free_hook < 2.34 Called on every free(), write system then free("/bin/sh")
GOT entries Partial RELRO Overwrite to redirect function calls
Stack return address Any If you can write to the stack
stdout _IO_FILE Any Leak addresses via _IO_2_1_stdout_ corruption

__malloc_hook and __free_hook were removed in glibc 2.34. For newer versions, exploitation typically targets _IO_FILE structures, the stack, or application-specific function pointers.

Checking glibc Version

# Check glibc version on the system
ldd --version | head -1

# Check which libc a binary uses
ldd ./binary | grep libc

# Get exact version from the library
strings /lib/x86_64-linux-gnu/libc.so.6 | grep "GNU C Library"

References

Tools

Official Documentation

MITRE ATT&CK