Every heap writeup I read when I started skipped the part I actually needed: the why. They'd say "now poison the tcache" like that was a sentence a human could parse. This is the post I wish I'd had — built around a deliberately vulnerable toy, not a real CVE, so you can reproduce every step yourself.
the allocator, in one paragraph#
glibc's malloc keeps freed chunks around to hand back fast. The fastest of
these caches is the tcache: a per-thread, per-size singly-linked list. Free a
chunk, it goes on the front of the list for its size. Allocate that size again,
you get the front chunk back. The list is threaded through the freed chunks
themselves — the first 8 bytes of a free chunk's data area hold the next
pointer.
That last sentence is the entire vulnerability surface. The link lives inside
memory you used to own. If you can write to a freed chunk, you can rewrite
next.
the toy#
|
The bug is boring and common: delete() frees but never nulls the pointer, so
edit() can still write through it. That's a use-after-free, and a UAF on a
tcache'd chunk is a write to the next link.
the poison#
The plan is three moves:
- Free a chunk so it lands in the tcache. Its
nextis whatever was there before (oftenNULLif the list was empty). - Write through the dangling pointer, overwriting
nextwith the address you wantmallocto eventually return. - Allocate twice. The first
mallochands back the chunk you freed; the second follows your forgednextand hands back your chosen address as if it were a fresh allocation.
Now you own a pointer to an address you picked. Point it at a
__free_hook, a GOT entry, a return address on a leaked stack — whatever your
target's mitigations leave standing — and you've converted a missing NULL
into control flow.1
then glibc moved the cheese#
Around glibc 2.32 the next pointer stopped being a raw address. Safe-linking
XORs it with a value derived from the chunk's own location:
So you can't just write &target anymore — you have to write
target XOR (chunk_addr >> 12). Which means you need a heap leak first, to
know chunk_addr. The primitive didn't die; it grew a prerequisite.
| glibc | tcache next |
what you need to poison |
|---|---|---|
| ≤ 2.31 | raw pointer | just the write |
| ≥ 2.32 | XOR-mangled | the write + a heap leak |
alignment check, too
Modern glibc also verifies that chunks coming out of the tcache are
16-byte aligned. A forged next pointing at some random unaligned address
will abort with "malloc(): unaligned tcache chunk detected". Pick targets
that are already aligned, or align them yourself.
what to actually take away#
The mechanics matter less than the mental model: freed memory is still attacker-controlled memory, and allocators keep their bookkeeping inside it. Every heap technique — fastbin dup, house of spirit, the whole "house of" bestiary — is a variation on lying to the allocator about its own metadata.
Get this one cold on a toy you built yourself before you touch a real target. Reproduce it under different glibc versions and watch the recipe change. The day safe-linking stops feeling like a wall and starts feeling like one more XOR you account for is the day heap pwn clicks.
-
Where you aim is the whole game and it's target-specific. The primitive is generic; turning it into a shell depends on what's writable and what's leaked. That's the part no tutorial can hand you. ↩