Fastbins keep freed chunks on a singly-linked LIFO list and don't consolidate
them — built for speed, so the checks are thin. The only thing standing between
you and freeing a chunk twice is a single comparison. Beat it and you get the same
chunk back from two different mallocs, which is an overlap, which is everything.
- arch
- amd64-64-little
- libc
- 2.27
- technique
- fastbin double-free -> overlapping alloc -> __malloc_hook
- tcache
- full (7 chunks parked first, so frees fall through to the fastbin)
the one check, and how it folds#
When you free a fastchunk, glibc guards against the laziest double-free:
It only checks whether the chunk you're freeing is already the head of the
fastbin. So freeing a twice in a row trips it — but slipping another free
between them does not:
The fastbin is now a cycle: a appears twice. pwndbg's fastbins shows it
plainly — the list eats its own tail:
you have to fill the tcache first
On glibc ≥ 2.26 free files chunks into the tcache before the fastbin, and
the tcache has its own (different) double-free defence. So a modern fastbin dup
starts by freeing 7 chunks of the same size to fill the tcachebin — only
then do further frees fall through to the fastbin where this trick lives.
hand out the duplicate, then lie about fd#
Now allocate the cycle back out. The third allocation returns a again, while
your first a pointer is still live — two handles to one chunk. Use one to
overwrite the chunk's fd (its forward pointer) and you redirect where the
fastbin's tail points:
|
the size trick#
A fastbin won't hand out a chunk whose size field doesn't match the bin — the
0x70 fastbin demands a size of 0x71 (or 0x70 with PREV_INUSE). Your fake
chunk needs that. The famous move on libc 2.23–2.26: aim __malloc_hook - 0x23,
because the misaligned bytes just above the hook happen to read as 0x7f, and
0x7f is a valid 0x70-fastbin size (the alignment isn't checked there yet):
00000000 00 00 00 00 00 00 00 00 00 00 00 7f 00 00 00 00 |................| 00000010 ac ee ee ad ae 07 10 7f |........ |
That 0x23 offset lands the fake chunk's size on that 0x7f and its data on
__malloc_hook. Overwrite the hook with a one_gadget (or
system) and the next malloc pops a shell.
the shell
what changed#
This recipe is a 2.23–2.27 staple. Two mitigations bent it out of shape:
- alignment check (2.32) — chunks pulled from tcache/fastbins must be
0x10-aligned, so the- 0x23misaligned-size trick is dead. You now need a genuinely aligned fake chunk with a real0x7f-class size nearby. - safe-linking (2.32) —
fdis stored as(&fd >> 12) ^ ptr, so poisoning it tofakerequires a heap leak to compute the mask first:mangled = (pos >> 12) ^ fake.
And __malloc_hook/__free_hook themselves were removed in glibc 2.34 — so
post-2.34 the same overlap primitive aims at a FILE struct or __exit_funcs
instead. The double-free is eternal; only the target keeps moving. Next:
unsafe unlink, where the doubly-linked lists give a write
without needing a hook at all.