Heap Exploitation

Stack overflows get all the headlines, but Use-After-Free (CWE-416) ranks #4 on the 2023 CWE Top 25 — with 44 known-exploited CVEs, more than SQL injection. The heap is where real programs keep their interesting data: dynamically allocated strings, C++ objects, structs full of function pointers. Learning to attack it is learning to attack the real world.

The heap vs. the stack

Before diving into attacks, make sure the terrain is clear.

Property Stack Heap
Lifetime Tied to the function call frame Controlled explicitly by the programmer (malloc / free)
Addressing %esp/%ebp + fixed offset Pointer returned by malloc
Managed by Compiler / CPU A user-space allocator library
Protection Stack canaries guard the return address No canary; no return address to protect
What lives there Local variables, saved registers, return address Dynamically-sized objects, structs, C++ objects

The heap sits above the BSS/data segments in the Linux 32-bit address space and grows upward (toward higher addresses), while the stack grows downward from the top of user space.

High address
  [ kernel space ]
  [ stack (grows ↓) ]
  ...
  [ heap  (grows ↑) ]
  [ BSS / data / text ]
Low address

malloc() is responsible for requesting pages from the OS (brk/mmap) and carving them into pieces the program can use.

How glibc malloc works: chunks and bins

On Linux, malloc is implemented by ptmalloc (pthreads malloc), an adaptation of Doug Lea's dlmalloc enhanced for multi-threading. Other implementations include Google's tcmalloc and Facebook's jemalloc, but ptmalloc is what you get on any standard Ubuntu/Debian system.

The malloc_chunk header

The basic unit ptmalloc manages is a chunk. Every allocation is wrapped in a small header:

struct malloc_chunk {
    size_t prev_size;   // size of the PREVIOUS adjacent chunk (when that chunk is free)
    size_t size;        // size of THIS chunk; low bits are flag bits
    struct malloc_chunk *fd;  // forward pointer — only used when chunk is FREE
    struct malloc_chunk *bk;  // back pointer   — only used when chunk is FREE
};

Key details:

When you call malloc(n), the pointer you receive points to the fd field (the first usable byte), not to the prev_size field. The header is just before the pointer you hold.

Free lists (bins)

ptmalloc keeps several bins — categorized free-chunk lists — to serve allocations quickly:

First-fit behavior: when you free() a normal chunk and then malloc() something of equal or smaller size, ptmalloc will hand back the same (or nearby) address from the unsorted bin.

char *a = malloc(300);   // 0x***010
char *b = malloc(250);   // 0x***150  (guards against coalescing with top)

free(a);

a = malloc(250);         // 0x***010 — same address! first-fit from unsorted bin

Attack 1: Heap buffer overflow

Buffer overflows work identically on the heap as on the stack — an unchecked write goes past the end of the allocated region. The difference is what you can hit:

The classic function-pointer overwrite

Consider two consecutive malloc calls:

struct data { char name[64]; };          // buffer; no function pointer
struct fp   { void (*fp)();              // function pointer
               char __pad[64 - sizeof(unsigned long)]; };

struct data *d = malloc(sizeof(struct data));
struct fp   *f = malloc(sizeof(struct fp));
f->fp = nowinner;

strcpy(d->name, argv[1]);   // no bounds check!
// ...
f->fp();                    // calls wherever fp points

Because d and f are allocated consecutively, d->name (64 bytes) sits immediately before f->fp in memory. Writing more than 64 bytes through strcpy overwrites f->fp with the attacker's chosen address — exactly the function-pointer hijack we saw on the stack, but now via heap adjacency.

This is why heap canaries don't exist: the threat model is different. There is no single critical slot like a return address; the dangerous data is application-specific (struct fields, vtable pointers, etc.).

Attack 2: Use-After-Free (UAF)

A use-after-free bug has three steps:

  1. Allocate memory and store a pointer to it.
  2. free() the memory — but keep the pointer (a dangling pointer).
  3. Later, use the dangling pointer to read or write.
char *data = malloc(20);
strcpy(data, "Hello, World!");
free(data);
// data is now a dangling pointer — the memory belongs to malloc again

strcpy(data, "New Data");    // undefined behavior: writing freed memory
printf("%s\n", data);        // undefined behavior: reading freed memory

Why UAF is exploitable

After free(data), ptmalloc may hand that same memory to the next malloc() call for a different object. If the attacker can control what gets placed in that recycled slot, they control the memory that the old dangling pointer sees. For example:

Combined with objects that contain function pointers, UAF becomes a control-flow hijack primitive.

Defending against UAF

The canonical fix is simple but often forgotten:

free(data);
data = NULL;   // null the pointer immediately; any later dereference segfaults safely

Tooling: Clang/LLVM's MemorySanitizer and AddressSanitizer instrument the binary to detect accesses to freed memory at run time — essential for finding UAF bugs during testing.

Attack 3: Corrupting malloc metadata (the free-list attack)

The most powerful heap attacks corrupt the allocator's own bookkeeping. Because fd and bk live inside the chunk data area when the chunk is free, a heap overflow that reaches a free chunk can overwrite those pointers.

The unsafe-unlink primitive (classic dlmalloc)

When two adjacent free chunks are coalesced, ptmalloc does roughly:

// remove chunk P from its doubly-linked bin list
P->bk->fd = P->fd;
P->fd->bk = P->bk;

If an attacker has corrupted P->fd and P->bk with chosen addresses, those two writes become:

*(attacker_addr_A + 8)  = attacker_addr_B
*(attacker_addr_B + 12) = attacker_addr_A

This is an arbitrary-write primitive: write a chosen value to a chosen address. Classically used to overwrite a GOT entry (Global Offset Table) with the address of shellcode or another function — turning a heap metadata corruption into a full control-flow hijack.

Safe-unlinking and modern mitigations

Modern ptmalloc added the safe-unlink check: before removing a chunk from a doubly-linked bin, it verifies P->fd->bk == P and P->bk->fd == P. If either check fails, the allocator aborts. This makes naive fd/bk corruption much harder, though not impossible (e.g., if the attacker can also set up a controlled pointer that passes the check).

Additional protections:

Heap vs. stack overflow: a quick comparison

Aspect Stack overflow Heap overflow
Target data Saved return address, saved %ebp Adjacent objects, struct fields, function pointers, allocator metadata
Classic defense Stack canary + NX Safe-unlink, allocator checks, ASLR
Trigger Write past a local buffer Write past a malloc'd buffer
Result Control %eip directly via ret Corrupt adjacent data; may need multiple steps to reach %eip

Key takeaways

Practice

  1. In glibc's ptmalloc, what are the fd and bk fields inside a malloc_chunk used for?
  2. What does the PREV_IN_USE flag (bit 0 of a chunk's size field) indicate?
  3. Why does a heap buffer overflow NOT require bypassing a stack canary?
  4. A program calls malloc(300), then free()s that pointer, then calls malloc(250). According to glibc malloc's first-fit behavior with the unsorted bin, what address is likely returned by the second malloc?
  5. In the heap-zero (Phoenix) example from lecture, two structs are allocated back-to-back: struct data (with a 64-byte name buffer) followed immediately by struct fp (with a function pointer). What is the attacker's goal?
  6. The 2023 CWE Top 25 places Use-After-Free (CWE-416) at rank #4 with 44 known-exploited CVEs. What does this tell us about heap exploitation?
  7. The classic unsafe-unlink attack corrupts a free chunk's fd and bk pointers before coalescing is triggered. What is the attacker's goal?
  8. Describe the three-step pattern that defines a use-after-free bug and explain how an attacker can exploit it to overwrite a security-sensitive value in memory.
  9. Modern glibc added the safe-unlink check to ptmalloc. What does it verify, and why does it make the classic fd/bk corruption attack harder (but not impossible)?