Most write primitives let you choose the value. This one doesn't — it always writes the same value (a libc pointer), to an address you pick. That asymmetry makes it feel useless at first. It isn't: aimed at the right global, "write a huge number here" rewrites the rules of the allocator itself.
- arch
- amd64-64-little
- libc
- 2.27 (the unsorted bin attack died in 2.29)
- technique
- unsorted-bin scan -> write a libc pointer to a chosen address
- primitive
- you control WHERE, not WHAT
the write hiding in the malloc loop#
When malloc scans the unsorted bin, it pulls the last chunk (victim) off the
list and splices it out with these lines:
That last line is the bug. bck comes straight from victim->bk, which you
control if you can corrupt a freed chunk. And bck->fd is *(bck + 0x10). So if
you set:
then bck = target - 0x10, and the allocator writes unsorted_chunks(av) — a
libc address — to *(target - 0x10 + 0x10) = *target. One freed chunk, one
overwritten bk, one malloc, and target now holds a libc pointer:
00000000 00 00 00 00 00 00 00 00 91 00 00 00 00 00 00 00 |................| 00000010 60 87 ec f7 ff 7f 00 00 18 c0 60 00 00 00 00 00 |`.........`.....|
(fd left intact pointing into libc; bk now points at target - 0x10.)
the value you can't choose#
The thing written is always unsorted_chunks(av) = &main_arena.bins[0] - 0x10
— a fixed offset into libc. You can't make it a gadget or a /bin/sh. So the
attack is only useful where a large libc-ish number is enough:
global_max_fast— the headline use. It's the cap on what counts as a fastbin-sized chunk. Overwrite it with a giant value and suddenly every size is "fast," so you can run a fastbin dup on large chunks (and the fastbin's checks are far thinner than the sorted bins'). This is the launch pad for several of the bigger Houses.- a size / loop bound — turn a bounded counter into an unbounded one.
_IO_list_all— the seed of the House of Orange, where the libc pointer you drop becomes the entry into a forged FILE chain.
you just corrupted the bin
After the write, the unsorted bin's bk points at target - 0x10, which is
not a real chunk. The next unsorted-bin operation will try to treat it as
one and likely crash. Do the attack, then immediately use what it bought you
(e.g. allocate from the now-poisoned fastbin) before touching the unsorted bin
again.
driving it#
|
where it lands you
With global_max_fast blown open, a fastbin dup on an arbitrary size drops a
chunk onto __free_hook; set it to system, free "/bin/sh":
segfault{wh3r3_n0t_wh4t}
modern status#
glibc 2.29 added an integrity check to the very loop above:
Since your forged bck (target - 0x10) doesn't have an fd pointing back at
victim, the check fails and the process aborts. So the unsorted bin attack is a
≤ 2.28 move — but its spirit lives on in tcache-era equivalents, and
global_max_fast is still a juicy target whenever you do land an arbitrary
write.
That closes the classic bins. Part 6 is the one you'll actually use on a modern
box: tcache poisoning — where the bin barely
checks anything and a single forged fd gives you allocation anywhere.