Heap Exploitation: Corrupting the glibc Allocator

10 min read

September 5, 2026

Site Updates

💬 Comments Available

Drop your thoughts in the comments below! Found a bug or have feedback? Let me know.

🚧 Recent Migration

Migrated from Ghost to Astro. Spot any formatting issues? Report them!

Heap Exploitation: Corrupting the glibc Allocator

Table of contents

Contents

👋 Introduction

Hey everyone!

Last week we drained databases with injection. This week we drop back down to memory corruption, picking up where the stack issue left off. Same discipline, different battlefield: the heap.

Stack overflows are linear and loud. You overflow a buffer, you smash the return address, and the game is control of a saved pointer. The heap gives you none of that. There is no return address next to your buffer, no obvious target. What there is instead is an allocator, a living data structure that glibc maintains on every malloc and free, threaded with pointers and size fields you can corrupt. Get the allocator to hand you a chunk that overlaps something it shouldn’t, and you turn a heap bug into an arbitrary read-write primitive.

This week: how glibc hands out memory, use-after-free, tcache poisoning past modern mitigations, the double-free dance, and the jump from an arbitrary write to a shell.

Let’s get into it 👇

🧱 How the Allocator Hands You Memory

Before you can corrupt the allocator, you need to know what it tracks. glibc’s malloc carves memory into chunks, and every chunk carries an 8-byte header: the size of the chunk, plus three low bits repurposed as flags because 16-byte alignment leaves them free. The most important is PREV_INUSE, which says whether the previous chunk is free.

When you free a chunk, glibc does not hand it back to the kernel. It parks it in a cache called a bin so the next malloc of that size is fast. The key bin is the tcache, a per-thread singly-linked list added in glibc 2.26, checked before everything else. A freed chunk stores the pointer to the next free chunk right in its own user data.

// A freed tcache chunk reuses its user data to store the freelist pointer
// [ size ][ fd -> next free chunk ][ ... ]
// malloc() of this size returns chunks by walking that fd pointer

When a bin has nothing to give, glibc carves your request off the top chunk, the single block of unused memory at the end of the heap. That is why fresh allocations grow linearly and freed ones get recycled out of order, and it is the layout you will spend the next sections bending to your will.

That last detail is the whole game. The allocator stores its freelist pointers inside the same memory it just handed to you, so a bug that writes into a freed chunk writes into allocator metadata. The realization to carry forward: on the heap you don’t attack the program’s data, you attack the bookkeeping that decides where the program’s data lives.

👻 Use-After-Free: The Pointer That Outlived Its Chunk

The cleanest heap bug is a pointer the program keeps using after it frees what the pointer points to. The chunk is back in a bin, but the program still holds its address, so you read and write freelist metadata through a live pointer.

The exploit comes from reallocation. You free an object, then allocate a new one of the same size, and glibc hands you back the exact chunk the dangling pointer still references. Now two pointers of different types alias the same memory.

char *a = malloc(0x40);   // object A
free(a);                  // A goes to tcache; `a` still points at it
char *b = malloc(0x40);   // allocator returns the SAME chunk to b
// writes through b now change what `a` reads, and vice versa

If object A held a function pointer or a length field, you overwrite it through B and the program keeps trusting A. This is the same heap-grooming discipline behind the browser exploitation in Issue 59, where controlling allocation order is what makes the overlap land where you want. The insight: a use-after-free is not a crash, it’s a type confusion you schedule by choosing what to allocate next.

🎯 tcache Poisoning: Aiming malloc at an Arbitrary Address

Here is the technique that makes the heap feel like a cheat code. If you can write into a freed tcache chunk, you overwrite its fd pointer, the “next free chunk” link. The allocator will hand that forged address back as a normal malloc return.

Two allocations and you have an arbitrary write anywhere. The first malloc pops the poisoned chunk, the second returns your target address, and now you write to it as if the program allocated it.

char *a = malloc(0x40); free(a);
*(size_t *)a = (size_t)⌖  // overwrite fd -> arbitrary address
malloc(0x40);                    // pops a
char *evil = malloc(0x40);       // returns &target: write anywhere

glibc fought back. Version 2.32 added safe-linking, which XOR-mangles every fd pointer with the chunk’s own address shifted right by 12, so you cannot forge a valid pointer without first leaking a heap address. It also demands the forged pointer be aligned. The takeaway: modern tcache poisoning needs a heap leak first, which is why the leak, exactly like the stack work in Issue 63, is worth more than the corruption itself.

🔁 Double-Free and the Fastbin Dance

Sometimes you can’t write into a chunk, but you can free it twice. A double-free puts the same chunk in a bin two times, so the allocator hands it out to two separate malloc calls, and you get the same overlap without needing a write primitive first.

glibc 2.29 added a check for this. Each tcache chunk now stores a key field, and on free glibc looks for that key to spot a chunk already sitting in the tcache. So the naive free-the-same-pointer-twice aborts.

// Blocked since glibc 2.29 by the tcache key check:
free(a); free(a);   // second free detects the key -> abort

// The bypass: cycle the chunk through fast bins, or corrupt the key
// so the second free looks like a fresh chunk to the allocator

The bypasses live in how2heap, which tracks every technique against the exact glibc version it works on. Fastbin dup abuses the fast bins, which never got the key check, to free a chunk twice with an intervening free of a different chunk. The mental model: each glibc release closes one door, and heap exploitation is the practice of knowing which door your target’s version left open.

🧩 Grooming the Heap

Every technique so far assumed the right chunks sit next to each other. On a real target they don’t, and the difference between a reliable exploit and a random crash is heap grooming, the practice of shaping the layout before you strike.

You drive the allocator with the program’s own features. Each request that creates an object is a malloc, each delete is a free, so you allocate and free in a chosen order until the heap holds the exact arrangement you need, your overflow source directly above the victim, or a freed hole waiting to catch your next allocation.

// Fill tcache with 7 chunks, then free the victim so it lands
// in a predictable slot the next malloc will hand back
for (int i = 0; i < 7; i++) malloc(0x40);   // pad the bin
free(victim);                               // now its position is known

The tcache holds seven chunks per size before overflow into other bins, so counting allocations lets you place a target deterministically. The order matters as much as the count, freeing in one sequence and reallocating in another is how you steer a specific chunk into a specific hole. This is the same feng shui the browser chains in Issue 59 rely on, moved from a JavaScript engine down to raw libc. The insight: heap bugs are probabilistic until you groom, and grooming is just using the program’s normal operations as an allocation script.

🐚 From Arbitrary Write to Shell

An arbitrary write is not a shell yet. For years the finish was easy: overwrite __free_hook or __malloc_hook, the writable function pointers glibc called on every free and malloc, with a one-gadget address, then trigger a free. glibc 2.34 removed both hooks, and that changed the endgame.

The modern targets are structures glibc still trusts. FSOP, or File Stream Oriented Programming, forges a _IO_FILE struct and its vtable so the next buffered output or the flush at exit calls your pointer. Corrupting the exit-handler list works the same way, hijacking a function the program runs on its way out.

The same file structures also solve the leak problem. Point _IO_2_1_stdout_ at a partial overwrite and the next puts prints libc data straight back to you, giving the address the rest of the chain needs. So the file streams cut both ways, they leak libc and they redirect control, which is why they became the default endgame the moment the hooks disappeared.

# one_gadget finds a single libc address that spawns a shell
one_gadget libc.so.6
# 0x50a37 execve("/bin/sh", rsp+0x40, environ)
#   constraint: [rsp+0x40] == NULL

This is not academic. The Baron Samedit sudo bug was a heap overflow in a command-line unescape, and Qualys turned it into reliable local root on default Ubuntu, Debian, and Fedora. The lesson that ties the issue together: the heap bug gives you a write, but a real exploit is a chain of writes aimed at whatever glibc still calls through a pointer, and that target list shrinks with every release.

🎯 Key Takeaways

The mental model to carry out of this issue: heap exploitation is metadata corruption, not data corruption. The stack gives you a return address to overwrite. The heap gives you an allocator whose freelist pointers live inside the chunks you control, so every technique here is really one idea, make malloc return a chunk that overlaps something it shouldn’t. Once you see the allocator as the target instead of the program, the whole class clicks.

Version is everything. tcache landed in 2.26, the double-free key check in 2.29, safe-linking in 2.32, and the malloc and free hooks vanished in 2.34. The exact same bug is trivial on one glibc and a research project on the next, so your first move against any heap target is to fingerprint its libc, because that dictates which technique is even possible.

The leak still outranks the corruption. Safe-linking means tcache poisoning needs a heap address, and the removal of the hooks means your write needs a known libc target, so an info leak is the load-bearing half of a modern heap exploit exactly as it was on the stack. Hunt the leak first and the write becomes arithmetic.

For the workflow: identify the bug class first, use-after-free or double-free or overflow, then check the glibc version to pick your technique. Reach for pwndbg or GEF to watch the bins and chunks live, how2heap to find the version-correct technique, libc-database to resolve offsets from a leak, and one_gadget to turn a single write into a shell. No hooks means aim the write at FSOP or the exit handlers instead.


Practice:


Thanks for reading, and happy hunting!

— Ruben

Other Issues

SQL Injection Deep Dive: Blind, Out-of-Band, and Shell
SQL Injection Deep Dive: Blind, Out-of-Band, and Shell

Previous Issue

Comments

Enjoyed the article?

Stay Updated & Support

Get the latest offensive security insights, hacking techniques, and cybersecurity content delivered straight to your inbox.

Follow me on social media