House of Storm

House of Storm - article cover image RE & Pwn
Time it takes to read this article 9 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 Storm is not a single allocator trick but a *composition* — it chains an unsorted bin attack with a large bin attack so that their two independent writes converge on the same fake chunk and hand the attacker the strongest primitive glibc’s soft-metadata allocator can give up: an arbitrary-address allocation. The next malloc() of the right size does not return heap memory at all; it returns a pointer to any writable address the attacker chose, whereupon an ordinary allocate-and-fill code path becomes a fully controlled write over __malloc_hook, a function pointer table, or a sensitive struct. The technique is prized precisely because it turns two modest, well-understood heap writes into a single clean ‘malloc anywhere’.

It is also firmly a historic primitive, and honesty about its era matters. House of Storm lives in the glibc window where the unsorted and large bins were still trusting: roughly glibc 2.23 through 2.26, extending onto early 2.27 with care. It was ground down by the same hardening that retired its two component attacks — most decisively the unsorted-bin integrity check if (__glibc_unlikely (bck->fd != victim)) malloc_printerr ("malloc(): corrupted unsorted chunks"); that landed in the 2.29 cycle, together with the large-bin fd_nextsize/bk_nextsize consistency checks. On any modern libc it does not work as originally published. Understanding it is still worthwhile: it is the canonical lesson in how two ‘weak’ writes combine into something far stronger than either alone, and the reasoning transfers directly to the modern house-of-* family.

Attack Prerequisites

House of Storm is intricate and version-fragile; it asks for more setup than most single-primitive houses. To stand it up you generally need:

  • A vulnerable libc in range — glibc ~2.23–2.26 (early 2.27 with care). On any build new enough to carry the bck->fd != victim unsorted check or the large-bin nextsize checks, the classic form aborts.
  • Control over one large-bin chunk and one unsorted chunk whose layout you can influence — an overlap or adjacency from a heap overflow, or a use-after-free that lets you rewrite freed chunk metadata.
  • The ability to edit chunk size and bk (and bk_nextsize) fields of those chunks, so you can steer both the unsorted-bin write and the large-bin write at a target address of your choosing.
  • A forged chunk header at the target whose size lands in the small-bin range with correct alignment and flag bits, so _int_malloc will service the request from it rather than splitting the top chunk.
  • On tcache-enabled glibc (2.26+), a filled tcache bin for the target size, so the request falls through the tcache fast path into the unsorted/large-bin machinery the attack actually drives.
  • A target address you can compute — typically a fixed libc target such as __malloc_hook on old libc, which needs a libc leak (or a static, non-PIE destination that needs none).

How It Works

The engine of House of Storm is the unsorted bin attack. When _int_malloc walks the unsorted bin looking for a chunk to return or sort, it unlinks the victim with the sequence bck = victim->bk; unsorted_chunks(av)->bk = bck; bck->fd = unsorted_chunks(av);. That last store is the whole point: it writes a *fixed libc address* — the unsorted bin head, which is an interior address of main_arena.bins[...] — into *(victim->bk + offsetof(struct malloc_chunk, fd)). Because the attacker controls victim->bk, this is a ‘write a known libc value to a chosen location’ primitive. On its own it is famously awkward: you can only write *that one value*, so historically it was used to smash global_max_fast and unlock fastbin games. House of Storm’s insight is to aim that write so it forges the size field of a fake chunk instead.

The large bin attack supplies the second, complementary write. When a chunk is inserted into a large bin, _int_malloc threads it into the size-ordered fd_nextsize/bk_nextsize skip list and updates the neighbours’ bk/bk_nextsize links. By pre-corrupting those link fields on a controlled large-bin chunk, the insertion writes an *attacker-chosen pointer* into a location of choice. In House of Storm this second write plants a pointer that makes the unsorted-bin machinery treat the attacker’s fake chunk as a legally linked small-bin candidate — the two writes are choreographed so that one supplies a value and the other supplies a link, and together they satisfy the checks _int_malloc performs while sorting.

The convergence happens in the sorting loop. The forged chunk is built so its size lands in the small-bin range with the right flag bits; the unsorted write and the large-bin write between them make _int_malloc believe this fake chunk is the correct small-bin entry for the incoming request. When the allocator finally satisfies the malloc(), it detaches and returns the fake chunk — but the fake chunk’s address is the attacker’s target. The result is an allocation sitting exactly on top of, say, __malloc_hook or a chosen writable struct. Nothing here forged a chunk out of thin air the way House of Spirit does; instead two real allocator writes were pointed at one address until the bin bookkeeping resolved in the attacker’s favour.

Vulnerable Code / Setup

House of Storm needs a program that (a) hands the attacker enough control over freed chunks to corrupt size/bk/bk_nextsize, and (b) is linked against a libc old enough to lack the unsorted and large-bin checks. A minimal menu-style harness with a heap overflow and a use-after-free is the usual vehicle:

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

/* Classic CTF-style note allocator: the edit path overflows, and freed
   pointers are never cleared -> UAF. Both are needed for House of Storm. */
static char *notes[16];
static unsigned sizes[16];

void create(int i, unsigned n) {
    notes[i] = malloc(n);         /* attacker chooses the size class      */
    sizes[i] = n;
}
void edit(int i, unsigned n, char *src) {
    /* VULN 1: n is not clamped to sizes[i] -> linear heap overflow into
       the adjacent chunk's size / bk / bk_nextsize fields.             */
    memcpy(notes[i], src, n);
}
void delete(int i) {
    free(notes[i]);               /* VULN 2: pointer left dangling (UAF) */
    /* notes[i] = NULL;  <-- the missing line that would kill the bug    */
}
C

The overflow lets the attacker rewrite the size, bk, and (for the large-bin chunk) bk_nextsize of a neighbouring freed chunk; the dangling pointer lets those corrupted chunks be re-processed by the allocator. Whether the technique is even reachable depends entirely on the libc version and the mitigations, so fingerprint before scripting:

$ checksec --file=./storm
RELRO      STACK CANARY   NX        PIE
Partial    Canary found   NX enab.  No PIE   <-- static __malloc_hook target ok

$ ldd ./storm | grep libc          # the whole attack hinges on this version
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (2.23)

# Sanity-check the version has NO 'corrupted unsorted chunks' bck->fd guard:
$ strings /lib/x86_64-linux-gnu/libc.so.6 | grep 'corrupted unsorted'
Bash

Walkthrough / Exploitation

Because House of Storm is a *composition*, drive it in a debugger first and watch each write land before you script it. In gdb with pwndbg, single out the unsorted-bin write (a libc address appearing at your target) and the large-bin link update, then the moment the fake small-bin chunk is returned:

gdb -q ./storm
pwndbg> break _int_malloc
pwndbg> run
pwndbg> unsortedbin           # confirm the victim chunk with your forged bk
pwndbg> largebins             # confirm the corrupted fd_nextsize/bk_nextsize
pwndbg> # step through the sorting loop:
pwndbg> x/4gx &main_arena     # watch bins[] head used by unsorted_chunks(av)
pwndbg> p &__malloc_hook      # the target the fake 'small' chunk overlaps
pwndbg> x/8gx <target>        # after malloc: allocation lands here
Bash

Against a real target the flow is: leak libc; groom the heap so a large-bin chunk and an unsorted chunk sit where you can corrupt them; forge the fake small-bin size at the target; corrupt bk (unsorted) and bk_nextsize (large bin); then request the size that forces _int_malloc down the unsorted/large-bin path. A pwntools skeleton, with every libc address resolved by symbol rather than a hardcoded number:

from pwn import *

io   = process('./storm')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6', checksec=False)

# --- menu wrappers ---
def create(i, n):        ...   # malloc(n) into slot i
def edit(i, data):       ...   # memcpy overflow into slot i's chunk
def delete(i):           ...   # free(slot i), pointer left dangling (UAF)
def show(i):             ...   # read back -> used for the libc leak

# 1) leak libc so __malloc_hook (and main_arena) are known
libc.address = leak_libc() - libc.sym['__libc_start_main']
target       = libc.sym['__malloc_hook']    # where we want malloc to return

# 2) on glibc 2.26+, fill the target-size tcache bin first so the request
#    falls through to the unsorted/large-bin path House of Storm drives.
fill_tcache(target_size)

# 3) forge the fake small-bin chunk header at (target - offset) and corrupt
#    the victim chunks: bk (unsorted write) + bk_nextsize (large-bin write)
edit(unsorted_idx, forge_unsorted_bk(target))
edit(largebin_idx, forge_largebin_links(target))

# 4) trigger: the sorting loop resolves both writes onto our fake chunk
create(99, target_size)          # <-- returns a pointer AT __malloc_hook
edit(99, p64(libc.sym['system']))# controlled write over the hook

# 5) cash it in: the next allocation error / free path fires the hook
trigger_malloc_hook()
io.interactive()
Python

The follow-through is whatever the arbitrary allocation overlaps. Classically the fake chunk is placed so it covers __malloc_hook/__free_hook, and the controlled write drops a one-gadget or system; on a non-PIE target with a static destination the same allocation can land on a GOT entry or a global callback table. The elegance — and the fragility — is that House of Storm produces an *allocation*, so any subsequent allocate-and-fill handler finishes the exploit for you.

Note: The most common reason a House of Storm build ‘almost works’ then aborts is the forged small-bin size: it must be aligned, carry the correct flag bits in its low nibble, and land in the small-bin range so _int_malloc services the request from your fake chunk rather than splitting the top chunk or re-sorting into a large bin. Verify with x/gx that the byte the unsorted write lands on is exactly the size field you intend, not an adjacent qword — the write targets victim->bk + offsetof(fd), so an off-by-one in the target pointer silently corrupts the wrong field.

Opsec: This is a version-locked technique. Before investing in the choreography, confirm the libc predates the bck->fd != victim ‘corrupted unsorted chunks’ guard (~2.29) and the large-bin fd_nextsize checks; on a hardened modern libc the same corruption instead trips malloc_printerr, which abort()s the process noisily and leaves an obvious SIGABRT / ‘malloc(): corrupted’ trail in logs. If you are on 2.29+, reach for a modern equivalent (large-bin attack into a writable target, or a house-of-* variant) rather than forcing House of Storm.

Detection and Defense

House of Storm is enabled by classic heap-metadata corruption — an overflow into freed chunk headers plus a dangling pointer — so the defenses are the durable ones for the whole soft-metadata class:

  • Keep a current glibc. The unsorted-bin bck->fd != victim check and the large-bin fd_nextsize/bk_nextsize consistency checks (2.29 era) directly break the technique; older libc that lacks them is the single biggest risk factor.
  • Null pointers after free and validate indices, removing the use-after-free that lets corrupted chunks be re-processed.
  • Eliminate the overflow — bound memcpy/read lengths to the allocation size so adjacent chunk size/bk/bk_nextsize fields cannot be rewritten.
  • Full RELRO + PIE + ASLR removes static targets (GOT) and forces a libc leak before any fixed destination such as __malloc_hook can be located.
  • Sanitizers in CI — AddressSanitizer catches the heap-buffer-overflow and the use-after-free deterministically during testing, well before the metadata games are possible.
  • Hardened allocators (scudo, hardened_malloc) keep metadata out-of-line and validate links, so the unsorted/large-bin write primitives simply do not exist.

Real-World Impact

House of Storm’s footprint is overwhelmingly in CTF and allocator research rather than in-the-wild CVEs, and its relevance is now largely pedagogical: it is the textbook demonstration that two individually limited primitives — the fixed-value unsorted-bin write and the pointer-planting large-bin write — can be composed into an arbitrary allocation strictly stronger than either. That composition mindset is exactly what carried forward into the modern house-of-* catalogue once the direct forms were patched out around glibc 2.29. The underlying weakness classes it rides on — heap overflow into freed metadata (CWE-122/CWE-787) and use-after-free (CWE-416) — remain perennial top-tier bugs, which is why the *reasoning* behind House of Storm outlived the specific byte sequence that made it fire.

Conclusion

House of Storm is the clearest case study in the heap-exploitation canon for how weak writes compound: an unsorted-bin attack that can only stamp one fixed libc value, married to a large-bin attack that can only thread one pointer, together resolve the allocator’s bin bookkeeping onto a fake small-bin chunk and yield ‘malloc at an arbitrary address’. It is genuinely historic — a creature of glibc 2.23–2.26 that the unsorted and large-bin integrity checks of the 2.29 era retired — and it should be studied as such. Learn it not to run it on a patched libc, but for the durable lesson it teaches: an allocator that trusts its own in-band links can have those links pointed at anything, and combining two of them is how a controlled allocation becomes control of the program.

You Might Also Like

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

Comments

Copied title and URL