algoatson.github.io ~/posts
segfault@algoatson.github.io
back to index

the House of Corrosion

2026-06-143 min readheap pwn glibc

The cruellest heap challenges give you no info leak and a stingy allocator — a handful of fixed-size chunks, no obvious read primitive. The House of Corrosion is the answer. It weaponises a single arena field so that the fastbin index arithmetic — normally a tiny lookup into an array of ten — starts indexing into libc's own writable data. From there you write your way to a leak, then a shell, having started with nothing.

target./corrosion
arch
amd64-64-little
libc
2.27
technique
global_max_fast overwrite -> fastbin index reaches into libc -> no-leak FSOP
premise
NO libc leak available, restrictive allocator

the arithmetic that becomes a write-what-where#

When you free a fastbin-sized chunk, glibc files it at fastbinsY[fastbin_index(sz)], where fastbin_index(sz) = (sz >> 4) - 2.

Normally sz ≤ global_max_fast (0x80), so the index stays inside the ten-entry fastbin array. But fastbinsY lives inside main_arena, which lives inside libc's writable data. So if you first overwrite global_max_fast with a huge value (an unsorted bin attack writes exactly the kind of giant value that does this), then a chunk of a much larger sz indexes far past the array — to a libc address you choose. Each fastbin slot is 8 bytes, so to land on target:

sz=2(targetfastbinsY)+32

Now free of a chunk with size sz writes that chunk's address (a heap pointer) to target — anywhere in libc's writable region. No leak needed: every offset here is a libc-version constant, measured from the arena.

corrosion.c — the primitivec
1
2
3
4
unsorted_bin_attack(&global_max_fast);     // global_max_fast = a giant libc pointer

size_t sz = ((target - fastbinsY) / 8 + 2) << 4;   // == 2*(target-fastbinsY)+32; indexes to target
free(chunk_of_size(sz));                   // *target = &chunk   (a heap pointer, in libc)

from a heap pointer in libc to a leak#

Writing heap pointers into libc sounds weak — you don't control the value. The move is to aim those writes at _IO_2_1_stdout_: corrode its _flags and its _IO_write_base/_IO_write_ptr so the next buffered puts/printf dumps a span of libc memory to you. That's your leak — manufactured from the writes themselves.

the staged corrosion of stdouttxt
1
2
3
free(size -> &stdout._flags)        # plant a pointer where _flags is read
free(size -> &stdout._IO_write_base)# widen the write window into libc
puts("...")                         # stdout now flushes libc bytes -> LEAK

you're freeing into libc — the checks still run

Each "free" is a real fastbin free: glibc still checks the chunk's size matches the (now absurd) fastbin index, that the chunk after it has a sane size, and the fasttop double-free. So the libc bytes around target have to already hold plausible values — much of the House of Corrosion is choosing targets whose neighbouring libc data passes those checks, and staging them in the right order. This is the most libc-version-sensitive technique in the series; the offsets are not portable across builds.

the kill#

With a leak in hand you're back on solid ground: forge a FILE for FSOP, or — on libc that still has them — drop system into __free_hook. Either way, you've gone from no leak and a stingy allocator to a shell, on the strength of one corrupted global_max_fast.

from nothing
txt
1
2
3
4
[*] global_max_fast = 0x7f...  (unsorted bin attack)
[*] corroded _IO_2_1_stdout_ -> libc leak: 0x7f3c1a2e4000
[*] forged FILE / __free_hook = system
segfault{n0_l34k_n0_pr0bl3m_3ith3r}

the end of the series#

That's the board, corner to corner: from a chunk's size field to forging FILE structs out of arena arithmetic. Fourteen techniques, one idea — malloc trusts its metadata, so every attack is just a more elaborate lie. Modern glibc has hardened most of these into specific eras (the checksec-style panel at the top of each part tells you when it died), but they remain the vocabulary: every new House is a recombination of these moves against whatever check shipped last release.

Go read the glibc source, keep pwndbg's vis open, and break some heaps.