House of Spirit

House of Spirit - 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 Spirit is the heap-exploitation primitive that runs the allocator backwards: instead of corrupting a chunk that malloc already handed out, the attacker *fabricates* a chunk in memory they already control — a stack frame, a .bss buffer, a global — and then persuades the program to free() a pointer into it. glibc’s free has no way to know the address it was given never came from malloc; it reads the forged size field, files the fake chunk onto a free-list, and a subsequent malloc of the matching size hands that same address straight back. The net effect is an allocation placed on top of memory the attacker chose, which is why the technique is the classic way to turn a controlled free argument into a write primitive over a return address or a sensitive global.

The technique is old — it dates to the original *Malloc Maleficarum* (2005) catalogue of ptmalloc attacks — but unlike several of its siblings it was never fully retired. Its two eras map onto glibc’s allocator history: the fastbin House of Spirit, which forges a small chunk plus the size field of the *following* chunk to satisfy the fastbin free checks, and the modern tcache House of Spirit (glibc >= 2.26), which is dramatically simpler because the tcache free path performs far fewer integrity checks than the fastbin path. Understanding which sanity checks each path applies is the entire discipline of getting a fake chunk accepted.

Attack Prerequisites

House of Spirit is a targeted primitive: it needs the program to free a pointer the attacker influences, plus enough write access to lay out a convincing fake chunk. Typical requirements:

  • A free() on an attacker-controlled pointer — a dangling index, a type-confused object, or any code path where the value passed to free can be steered to memory the attacker owns.
  • Write access to that memory to forge the chunk header: a valid size field (with the correct in-use/arena flag bits) immediately before the pointer that will be freed.
  • For the fastbin variant, a second forged size field — the size word of the *next* chunk (at fake + size) must itself pass a sanity range check, so you forge two size fields, not one.
  • Size control so the forged size lands in the intended bin — the fastbin range (up to 0x80 on 64-bit by default) for the fastbin variant, or any tcache-eligible size (up to 0x410) for the tcache variant.
  • Knowledge of the target address, which usually means a stack or heap leak if the destination is ASLR-affected; a static .bss/.data target on a non-PIE binary needs no leak.

How It Works

A glibc chunk is just a size field followed by user data; a *free* chunk of small size additionally stores forward-link pointers in that user data. free() decides where a chunk goes purely from its size. In the fastbin path, _int_free computes the fastbin index from the size and pushes the chunk onto the corresponding singly-linked LIFO list — but first it applies two checks that House of Spirit must satisfy. The chunk’s own size must be aligned and fall inside the fastbin range, and the size field of the *following* chunk (found at chunk + size) must be greater than 2*SIZE_SZ and less than the arena’s system_mem. That second check is why the fastbin forgery needs a second, downstream size word: without it free aborts with “invalid next size”.

Once the fake chunk is on the fastbin, the attack is finished by allocation. A malloc request for exactly that size pops the fake chunk off the list and returns a pointer to it — but that pointer now aliases the attacker’s original buffer. If the fake chunk was laid over a stack frame, the program is now writing ‘heap’ data directly onto the stack, over saved registers and the return address. The allocator never validated that the address belongs to the heap; it only ever trusted the size fields, and those were forged.

The tcache variant is the one that matters on modern systems and it is much easier. When tcache is in play (glibc >= 2.26, default), free first tries to place a small chunk into the per-thread cache. That path historically checked essentially nothing about the chunk beyond deriving a bin index from its size; later releases added an alignment check on the chunk pointer and a guard that the derived tcache index is in range, but there is still no ‘next chunk size’ requirement as in the fastbin path. So the tcache House of Spirit needs only a single forged, correctly-aligned size field: forge it, free the pointer, and the very next malloc of that size returns the fake chunk. This simplicity is exactly why the technique survived the tcache era while flashier houses were mitigated away.

Vulnerable Code / Setup

The bug that enables House of Spirit is a free() whose argument the attacker controls, typically because a menu-driven allocator lets you free an index whose pointer you can also influence, or because an object pointer is confused with an attacker-supplied value. A minimal, self-contained demonstration frees a pointer into a stack buffer directly:

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

/* Attacker goal: get malloc to return a pointer INTO this stack frame. */
void vuln(void) {
    uint64_t fake[16];            /* the memory we will forge a chunk in   */

    /* Forge a tcache-eligible chunk header. On 64-bit, a request of 0x20
       user bytes lives in a chunk of total size 0x30. The size word sits
       just before the user data that free() receives. */
    fake[1] = 0x31;               /* chunk size = 0x30 | flags (aligned)   */

    void *p = &fake[2];           /* pointer to the 'user data' region     */
    free(p);                      /* VULN: freeing a non-heap pointer      */

    /* The fake chunk is now in tcache[0x30]. The next same-size malloc
       returns &fake[2] -- an allocation living on the stack. */
    char *q = malloc(0x20);       /* q == &fake[2]                         */
    printf("malloc returned %p (stack buffer at %p)\n", q, fake);
}
C

For the fastbin variant you must additionally forge the *next* chunk’s size so free accepts it — the second size word below is what passes the “next size” range check:

uint64_t fake[16];
fake[1] = 0x71;      /* our fake chunk: size 0x70, a fastbin size        */
fake[1 + (0x70/8)] = 0x71;  /* NEXT chunk's size: must be > 2*SIZE_SZ and
                               < system_mem, or free() aborts.           */
free(&fake[2]);      /* accepted onto fastbin[0x70]                       */
C

Whether the technique is even reachable depends on the binary’s mitigations, so triage with checksec first. A non-PIE target with a static .bss/.data destination needs no leak; a PIE or stack target requires a corresponding address leak before the fake chunk can be located:

$ checksec --file=./target
RELRO      STACK CANARY   NX        PIE
Full RELRO Canary found   NX enab.  No PIE   <-- .bss target, no leak needed

$ ldd ./target | grep libc     # fingerprint libc: tcache vs fastbin path
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (2.31)
Bash

Walkthrough / Exploitation

Work the fake chunk in a debugger before scripting the exploit — the whole game is confirming that free accepted your forged header and that the bin now points at your target. In gdb with pwndbg, watch the tcache/fastbin bins update as you free the fabricated pointer:

gdb -q ./target
pwndbg> break vuln
pwndbg> run
pwndbg> # after forging fake[] and stepping over free():
pwndbg> tcachebins        # fake chunk should appear in the 0x30 bin
pwndbg> fastbins          # (fastbin variant) fake chunk in the 0x70 bin
pwndbg> x/8gx &fake       # confirm the two forged size words are correct
Bash

Against a realistic menu-style program the attack composes as: (1) leak an address of the region you will forge in, if it is randomized; (2) write the fake chunk header(s) into that region; (3) drive the program to free a pointer to the fake user data; (4) request the matching size so malloc returns the fake chunk; (5) write through the returned pointer to overwrite the target. A pwntools skeleton:

from pwn import *

io = process('./target')

# --- menu wrappers ---
def alloc(sz, data): ...   # request a chunk and fill it
def free_ptr():      ...   # trigger free() on the controlled pointer
def set_ptr(addr):   ...   # steer the freed pointer to our fake chunk

stack_leak = leak_stack()          # e.g. via a format-string or info leak
fake = stack_leak + 0x40           # where we will forge the chunk

# 1) forge the header: size 0x30 chunk (tcache variant), aligned + flags
write_qword(fake + 0x08, 0x31)     # size word

# 2) point the program's pointer at the fake user data and free it
set_ptr(fake + 0x10)
free_ptr()                         # fake chunk -> tcache[0x30]

# 3) reclaim it; the returned allocation now overlaps our chosen memory
alloc(0x20, p64(0xdeadbeef))       # write lands at fake+0x10 (our target)
Python

The follow-through depends on the destination. A fake chunk placed so that the returned allocation overlaps a saved return address turns the next write into a stack overwrite for a ROP chain; one placed over a global function pointer or a struct’s callback redirects control on the next indirect call. Because House of Spirit yields an *allocation*, not a direct write, the primitive is at its cleanest when the program writes user-controlled bytes into freshly malloc‘d memory soon after allocation — which the vast majority of allocate-and-fill menu handlers do.

Note: The single most common reason a House of Spirit free() aborts is the alignment/size relationship, not the value of the size itself. The forged size must be 16-byte aligned on 64-bit (the low nibble holds only the PREV_INUSE/IS_MMAPPED/NON_MAIN_ARENA flag bits), and for the fastbin variant the *next* chunk’s size must satisfy 2*SIZE_SZ < sz < system_mem. When forging on the stack, remember the fake chunk’s flags and the downstream size word may collide with live local variables — lay the fake chunk in a scratch buffer you fully control, not across the saved frame you intend to overwrite later.

Opsec: House of Spirit is version-sensitive on the free path: the fastbin variant needs the two-size forgery on any glibc, while the tcache variant needs only one size word but must satisfy the alignment guard and (on newer glibc) an in-range tcache index. Fingerprint the target libc before choosing a variant. In gdb, tcachebins/fastbins immediately after the free are the fastest confirmation that the allocator swallowed the fake chunk; if the bin is empty, the size word or an integrity check rejected it.

Detection and Defense

House of Spirit is fundamentally enabled by passing an untrusted pointer to free, so the durable defenses are the ones that prevent that or make the forged header impossible to satisfy:

  • Never free attacker-influenced pointers — validate that a pointer came from a matching allocation, and null out pointers after free so a dangling index cannot be re-freed into a forged region.
  • Keep allocator hardening intact — modern glibc’s tcache free path checks chunk-pointer alignment and rejects an out-of-range tcache index; the fastbin path enforces the next-size range. Do not run ancient libc that omits these.
  • Full RELRO + PIE + ASLR — removes easy static targets (the GOT) and forces the attacker to leak the address of any stack/heap region they want to forge in, raising the cost substantially.
  • Sanitizers in CI — AddressSanitizer flags free of a non-heap or mismatched pointer deterministically (“attempting free on address which was not malloc’d”), catching the primitive during testing.
  • Hardened allocators — allocators that keep metadata out-of-line and validate chunk provenance (e.g. hardened_malloc, scudo) reject a fabricated in-band header outright.

Real-World Impact

House of Spirit is most visible today as a workhorse in CTF pwn and in allocator-research write-ups, precisely because it is the simplest way to convert a controlled free() into an allocation over chosen memory. Its conceptual footprint is broader than glibc: the same ‘free a fake chunk’ idea recurs anywhere an allocator trusts an in-band, caller-supplied header, and it underlies a family of real-world bugs where a type confusion or an out-of-bounds index causes a non-heap pointer to reach a free-like routine. The bug classes it depends on — controlled-pointer frees, double-free, and type confusion — sit near the top of industry weakness lists (CWE-416, CWE-415, CWE-843), and the reason the tcache variant remains relevant is that, unlike House of Force or the classic unsorted-bin attack, no single glibc release ever closed it outright.

Conclusion

House of Spirit inverts the allocator’s trust model: by forging a chunk header in memory it controls and inducing a free, the attacker convinces malloc to serve an allocation on top of a stack frame, a global, or any chosen buffer. The fastbin variant pays for this with a two-size forgery to pass the next-size check; the tcache variant needs only one aligned size word, which is why it endures on modern glibc. The technique is a clean reminder that an allocator is only as trustworthy as the provenance of the pointers it is asked to free — validate that provenance, keep the hardening checks on, and the fabricated chunk never gets filed.

You Might Also Like

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

Comments

Copied title and URL