House of Prime

House of Prime - 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 Prime is one of the original heap-exploitation primitives catalogued in Phantasmal Phantasmagoria’s *The Malloc Maleficarum* (2005), the paper that first systematised attacks against glibc’s ptmalloc after the classic unlink() write was hardened. Where later ‘houses’ target tcache or the top chunk, House of Prime aims squarely at the machinery that governs the fastbins — specifically the per-arena ceiling variable that decides which chunk sizes are eligible for the fast path. By corrupting that ceiling and then abusing the way free() computes a fastbin index from a chunk’s size, the attacker persuades the allocator to file an oversized, attacker-shaped chunk onto a fastbin and hand it back on the next malloc, producing an allocation over memory of the attacker’s choosing.

It must be said up front, and without hedging: House of Prime is historic. It was written against the pre-2.4-era arena layout in which the fastbin ceiling lived immediately adjacent to the fastbin array with no size sanity checks between them, and it does not work on any modern glibc. Decades of hardening — renaming and relocating the ceiling to today’s global_max_fast, adding fastbin size validation, and eventually the tcache — have retired it completely. Its value now is pedagogical: understanding House of Prime is the cleanest way to internalise how fastbin_index(), max_fast, and the fastbinsY array interact, and it is the direct ancestor of the modern, leakless House of Corrosion, which reincarnates the same ‘abuse the fastbin ceiling and index arithmetic’ idea against contemporary global_max_fast.

Attack Prerequisites

House of Prime is a two-free primitive with tight layout requirements. On the glibc it was designed for, the attacker needs enough control to forge chunk size fields and drive two frees in sequence:

  • A heap primitive that lets you control chunk size fields — an overflow into an adjacent chunk header, or a use-after-free / double-free that lets you re-shape a freed chunk’s size before it is freed again.
  • The ability to free two attacker-shaped chunks in a chosen order — the first free corrupts the fastbin ceiling; the second exploits the now-relaxed ceiling to bank an oversized chunk on a fastbin.
  • The historic arena layout in which the fastbin ceiling (max_fast) is adjacent to the fastbinsY array and is writable through an under-indexed fastbin slot — i.e. an old glibc with no fastbin size checks.
  • A target region the second allocation should land on — classically a writable arena field or a function pointer; on a non-PIE binary a static target needs no leak, otherwise an address leak is required.
  • Absence of the modern mitigations — no tcache key/double-free detection, no global_max_fast separation, no fastbin chunksize sanity check.

How It Works

Fastbins are singly-linked LIFO lists of small chunks that free() services without coalescing, for speed. When a chunk is freed, _int_free first asks whether its size is at or below the arena’s fastbin ceiling — historically the variable max_fast, the direct ancestor of today’s global_max_fast. If so, it derives an index with fastbin_index() and pushes the chunk onto fastbinsY[index]. On 64-bit modern glibc that index is computed as (sz >> 4) - 2; the exact shift differs across eras, but the essential point is that the index is a simple arithmetic function of the size, performed with no lower-bound guard in the old code. That missing guard is the whole bug: a carefully undersized size field makes fastbin_index() produce a small or negative index, so the ‘slot’ the allocator writes into is not a real fastbin at all but an adjacent arena field.

House of Prime chains two frees around that observation. The first free is the setup: the attacker frees a chunk whose size has been crafted so that its computed fastbin index underflows and aliases the max_fast field itself. Because a fastbin free stores the chunk’s own address (a heap pointer) into fastbinsY[index], that store lands on max_fast instead, overwriting the ceiling with a large heap-derived value. In one free, the maximum size the allocator will treat as ‘fast’ has jumped from the default (0x80-class on 64-bit) to something enormous.

The second free cashes in. With max_fast now huge, the attacker frees a *large* chunk whose size would normally be far too big for any fastbin. It is now accepted, and fastbin_index() files it at an attacker-influenced index. Because the attacker controls the large chunk’s size, they control which fastbinsY slot — or which memory past the end of the array — receives the chunk, and they control the forward-link the allocator will follow. The next malloc of the matching size walks that poisoned list and returns a pointer into attacker-chosen memory: a fake chunk overlapping an arena field, a function pointer, or any writable target. From there the classic finish is to overwrite a hook or an arena pointer and redirect control flow. Every step depends on the allocator trusting a size field it never validated.

Vulnerable Code / Setup

House of Prime does not spring from one line of C so much as from a program that lets the attacker shape chunk sizes and free them repeatedly. A minimal menu-style allocator with an off-by-one / overflow into the next chunk’s size field is the canonical enabler — it hands the attacker exactly the size control the two-free chain needs:

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

/* Toy vulnerable allocator: a heap overflow lets us overwrite the SIZE
   field of a neighbouring chunk, which is all House of Prime needs to
   forge the two crafted frees. */
static char *slots[16];

void alloc(int i, size_t n) { slots[i] = malloc(n); }
void freep(int i)           { free(slots[i]); }        /* two of these drive the attack */

void edit(int i, char *src, size_t n) {
    /* VULN: no bound on n -> overflow into slots[i+1]'s chunk header,
       letting us set an undersized size (free #1) or an oversized size
       (free #2) before each free. */
    memcpy(slots[i], src, n);
}

/* On the historic glibc this targets, free() derives fastbin_index(sz)
   with no lower bound; an undersized sz makes the index alias max_fast,
   and the subsequent store overwrites the fastbin ceiling. */
C

The technique is entirely dependent on the libc version, so the first triage step is fingerprinting the target and confirming the mitigation surface. On a modern distro this is where you discover House of Prime is a museum piece — the loaded libc has tcache, global_max_fast, and fastbin size checks:

$ checksec --file=./target
RELRO      STACK CANARY   NX        PIE
Partial    No canary      NX enab.  No PIE   <-- static target, no leak needed

$ ./target &   ldd ./target | grep libc     # fingerprint the allocator era
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (2.35)   # <-- HoP will NOT work here

# In gdb, confirm the modern ceiling symbol exists and is separated from the
# arena's fastbinsY -- its presence is why House of Prime is dead today:
pwndbg> p &global_max_fast
pwndbg> p &main_arena.fastbinsY
Bash

Walkthrough / Exploitation

Because the technique is historic, treat the walkthrough as a reconstruction of the 2005 chain against a matching old glibc, not something you would deploy on a current box. The discipline is the same as any fastbin attack: verify in the debugger that free #1 actually moved the ceiling before you attempt free #2. In gdb with pwndbg, watch max_fast/global_max_fast and the fastbins as you go:

gdb -q ./target
pwndbg> break freep
pwndbg> run
pwndbg> # --- before free #1: inspect the crafted undersized size field ---
pwndbg> heap
pwndbg> p global_max_fast        # note the current ceiling value
pwndbg> # --- step over free #1, then re-check: the ceiling should be huge ---
pwndbg> finish
pwndbg> p global_max_fast        # corrupted to a large (heap-derived) value
pwndbg> fastbins                 # the store aliased the ceiling, not a real bin
pwndbg> # --- free #2: an oversized chunk is now accepted onto a fastbin ---
pwndbg> fastbins                 # attacker-indexed slot now holds our chunk
Bash

Scripted against a menu program, the chain composes as: shape the undersized size and free once to lift the ceiling; shape the oversized size and free again to bank the large chunk on a controlled fastbin index; then allocate to receive a pointer into the target. A pwntools skeleton — using symbol lookups rather than hardcoded offsets, exactly as you should for any libc-specific attack:

from pwn import *

libc = ELF('./libc.so.6', checksec=False)
io = process('./target')

# --- menu wrappers ---
def alloc(i, n):        ...   # request a chunk into slot i
def edit(i, data):      ...   # overflow: overwrite neighbour's size field
def free_slot(i):       ...   # trigger free() on slot i

# 1) Free #1: craft an UNDERSIZED size so fastbin_index() underflows and the
#    freed chunk's address is stored over the fastbin ceiling (max_fast).
edit(0, forge_undersized_header())   # size chosen so index aliases the ceiling
free_slot(1)                          # ceiling now blown wide open

# 2) Free #2: with the ceiling huge, an OVERSIZED chunk is accepted onto a
#    fastbin at an attacker-influenced index; we steer its fd toward the target.
edit(2, forge_oversized_header())     # large size -> chosen fastbinsY slot
free_slot(3)

# 3) Reclaim: the next same-size malloc walks the poisoned list and returns a
#    pointer into attacker-chosen memory (an arena field / function pointer).
alloc(4, 0x80)                        # allocation now overlaps the target
# ... write through slot 4 to overwrite the target and redirect control flow.
Python

The follow-through in the original era was to land the reclaimed allocation on a writable arena field or a global function pointer and overwrite it — the malloc/free hooks were the obvious destinations before they were removed in glibc 2.34. The key mental model to carry forward is that House of Prime never needed a linear write to the ceiling: it manufactured that write purely from free()‘s own index arithmetic applied to a size the allocator forgot to range-check. That is precisely the lever the modern House of Corrosion pulls against global_max_fast.

Note: The single most important thing to understand about House of Prime is *why it is dead*, because that reveals the mitigations by name. Modern glibc renamed and relocated the ceiling to global_max_fast (a single global, no longer sitting exploitably adjacent to a writable fastbinsY), added a chunksize sanity check on the fastbin free path, and interposed the tcache in front of the fastbins entirely. Any one of these breaks the two-free chain; together they retire it. If you are studying this on a current box, expect free() to abort() long before the ceiling moves.

Opsec: Do not confuse House of Prime with House of Corrosion. Prime is the 2005 *Malloc Maleficarum* ancestor against the old max_fast/arena layout and will not run on anything you will meet in the field. Corrosion is the modern, leakless descendant that abuses global_max_fast and fastbin indexing on contemporary glibc. When you see ‘fastbin ceiling corruption’ in a writeup, check the glibc version before assuming which technique — and which set of checks — is actually in play.

Detection and Defense

House of Prime is a solved problem: the defenses are baked into every supported glibc, and the residual advice is to not run a decades-old allocator and to keep the general anti-heap-corruption posture that stops its modern cousins:

  • Use a current glibc — the separation of global_max_fast, fastbin chunksize validation, and the tcache layer all postdate and defeat the technique; ancient libc that omits them is the only place it lives.
  • Prevent the size-field corruption that seeds it — the two-free chain starts from an overflow or UAF that lets the attacker shape chunk sizes; bounds checking and freeing-then-nulling pointers remove the precondition.
  • Keep allocator hardening on — tcache double-free keys and fastbin integrity checks catch the crafted frees at runtime; do not build against patched-out or disabled hardening.
  • AddressSanitizer / sanitizers in CI — an overflow into a neighbouring chunk header or a mismatched free is flagged deterministically during testing, long before it reaches a fastbin.
  • Hardened allocators — allocators that keep metadata out-of-line and validate chunk provenance (hardened_malloc, scudo) never expose an in-band ceiling to corrupt in the first place.

Real-World Impact

House of Prime’s real significance is historical and educational rather than operational. As one of the *Malloc Maleficarum* houses it helped define the research agenda for glibc heap exploitation and demonstrated, years before tcache poisoning made it routine, that an allocator’s own index arithmetic could be weaponised into an arbitrary write when a single range check was missing. You will not find it powering a current CVE — the underlying bug classes it rides on (heap overflow, use-after-free, double-free; CWE-122, CWE-416, CWE-415) remain at the top of the industry weakness lists, but the specific fastbin-ceiling mechanism was closed long ago. Its living legacy is House of Corrosion, which shows the same idea remains dangerous whenever the modern global_max_fast can be reached.

Conclusion

House of Prime is best understood as the fastbin ceiling attack in its original 2005 form: two crafted frees, the first abusing an unguarded fastbin_index() to overwrite max_fast, the second banking an oversized chunk onto a controlled fastbin for an arbitrary allocation. It is thoroughly patched — the relocation to global_max_fast, fastbin size checks, and the tcache each break it — so treat it as a lesson rather than a tool. But it is a lesson worth learning, because the exact lever it pulls, corrupting the fastbin ceiling and letting the allocator’s index math do the writing, is alive and well in its modern descendant House of Corrosion. Learn Prime to understand the machinery; study Corrosion to see where that machinery can still be broken.

You Might Also Like

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

Comments

Copied title and URL