House of Force

House of Force - article cover image RE & Pwn
Time it takes to read this article 8 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 Force is the heap technique that weaponises the *top chunk* — the single large block of unused memory (the ‘wilderness’) from which malloc carves new allocations when no free chunk fits. The idea is disarmingly direct: if an attacker can overwrite the size field of the top chunk with an enormous value, the allocator will believe the wilderness is effectively infinite. A subsequent request for a carefully chosen, very large size then advances the top-chunk pointer by that amount — and because pointer arithmetic wraps, the *next* allocation can be made to land at an arbitrary address the attacker computes. One overflow into the top size plus two mallocs yields an allocation anywhere in the address space.

For years House of Force was a staple because it needed only three things: a linear overflow reaching the top chunk, the ability to request an attacker-chosen (large) allocation size, and knowledge of the top-chunk address and a target address. It works on glibc from the 2.23 era through 2.28. Its story is also one of the cleanest examples of a mitigation ending a technique: glibc 2.29 (2019) added a sanity check on the top chunk’s size in the allocation path, and that single check neutralised the classic House of Force outright. Studying it is therefore as much a lesson in how allocator hardening kills a primitive as it is in the primitive itself.

Attack Prerequisites

House of Force is unusually demanding about *what* you can overwrite and *how large* a request you can make, but forgiving about ordering. The classic form needs:

  • A heap overflow that reaches the top chunk’s size field — typically a linear overflow out of the last allocated chunk, letting you overwrite the wilderness header that sits immediately after it.
  • The ability to overwrite that size with a huge value — canonically 0xffffffffffffffff (all bits set) so the top chunk appears large enough to satisfy any request.
  • Control over an allocation size — you must be able to request a specific, large malloc size, because that size is the ‘distance’ the top pointer is moved.
  • Knowledge of the top-chunk address and the target address — the request size is computed as target - top - 2*SIZE_SZ, so both must be known (no ASLR, or a heap leak plus a target whose address you can compute).
  • glibc <= 2.28 — on 2.29 and later the top-size sanity check aborts the oversized request before it can move the wilderness.

How It Works

When _int_malloc cannot satisfy a request from any bin, it carves the request out of the top chunk: it checks that the top chunk’s size is at least the requested size plus MINSIZE, then splits off the request and advances the top pointer to old_top + nb (where nb is the padded request size), writing a fresh, smaller size into the new top. House of Force attacks the ‘is the top big enough?’ comparison. By overwriting the top size with 0xffffffffffffffff, the check top_size >= nb + MINSIZE passes for *any* nb, so no request is ever too large — the allocator will always try to serve from the wilderness.

With that guard defeated, the attacker controls where the top pointer moves. The allocator computes the new top as old_top + nb. If you want the *next* allocation to be returned at some target, you request a size that moves the top pointer to exactly target - 2*SIZE_SZ (so the following chunk’s user data starts at target). Solving for the request gives the canonical House of Force distance: nb = target - (old_top + 2*SIZE_SZ). Because unsigned pointer arithmetic wraps modulo 2^64, this works even when target is at a *lower* address than the heap — the request is simply a huge value that overflows the pointer back around to the destination. The malloc immediately after this one then returns a chunk whose user data begins at target.

The takedown in glibc 2.29 is a single, well-placed check. When the allocator prepares to serve from the top chunk, it now validates that the top chunk’s size does not exceed the total memory the arena has obtained from the system — conceptually if (top_size > av->system_mem) abort("malloc(): corrupted top size"). An all-ones (or otherwise absurd) top size can no longer survive to the allocation step: the process aborts the instant the poisoned wilderness is used. This is why House of Force is described as *neutralised* rather than merely *constrained* on modern glibc — the very condition the technique depends on (an impossibly large top size) is now the condition that triggers the abort.

Vulnerable Code / Setup

The enabling bug is a linear heap overflow out of a chunk that sits directly before the top chunk, letting the overflow reach and rewrite the wilderness size. A minimal reproduction on a vulnerable (<= 2.28) glibc:

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

int main(void) {
    /* p is the last allocated chunk; the top chunk header sits right after
       its user data. An overflow out of p reaches the top size field. */
    char *p = malloc(0x18);

    /* BUG: unbounded copy overflows p and overwrites the top chunk size
       with 0xffffffffffffffff (the House of Force setup). */
    char payload[0x28];
    memset(payload, 'A', 0x18);
    *(unsigned long *)(payload + 0x18) = 0xffffffffffffffffUL; /* top size */
    memcpy(p, payload, 0x20);   /* linear overflow into top header */

    /* 1) request that moves top to (target - 0x10); 2) reclaim at target */
    unsigned long distance = /* target - (top + 0x10) */ 0;
    malloc(distance);           /* advances the wilderness pointer */
    void *evil = malloc(0x18);  /* returned at 'target' */
    printf("evil chunk at %p\n", evil);
}
C

The distance is arithmetic, not magic: distance = target - top_addr - 2*SIZE_SZ, evaluated modulo 2^64. Both addresses must be known, so triage the binary’s mitigations to decide whether a leak is required. On a non-PIE binary with a static .bss target and predictable heap, no leak is needed:

$ checksec --file=./hof
RELRO      STACK CANARY   NX        PIE
Partial    No canary      NX enab.  No PIE   <-- static targets available

$ ldd ./hof | grep libc     # MUST be <= 2.28 for classic House of Force
    libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (2.27)
Bash

Walkthrough / Exploitation

Confirm the top-chunk corruption in a debugger before computing the distance — the top chunk address is what the whole calculation hinges on. In gdb with pwndbg, read the wilderness before and after the overflow:

gdb -q ./hof
pwndbg> run
pwndbg> top_chunk         # address + size of the wilderness
pwndbg> vis_heap_chunks   # see p, then the top chunk header after it
pwndbg> # after the overflow, the top size should read 0xffffffffffffffff
pwndbg> x/4gx <top_addr>  # verify the poisoned size word
Bash

The exploit then computes the request that walks the wilderness to just before the target and takes the target with the following allocation. A pwntools skeleton makes the arithmetic explicit:

from pwn import *

io = process('./hof')
SIZE_SZ = 8

def alloc(sz, data=b''): ...   # menu wrapper: request a chunk, fill it
def overflow(data):      ...   # linear overflow out of the last chunk

top   = heap_leak + 0x20           # address of the top chunk (from a leak)
target = elf.sym['target_global']  # where we want the next chunk returned

# 1) poison the top chunk size with all-ones
overflow(b'A'*0x18 + p64(0xffffffffffffffff))

# 2) request the exact distance so top advances to target-0x10
distance = (target - (top + 2*SIZE_SZ)) & (2**64 - 1)
alloc(distance)                    # moves the wilderness pointer

# 3) the next allocation is returned AT target
alloc(0x18, p64(0xdeadbeef))       # write lands at target
Python

Where you point target decides the outcome. On glibc versions that still carried them, a favourite destination was __malloc_hook (overwrite it with a one_gadget so the *next* malloc pops a shell); on Partial-RELRO binaries a GOT entry works; and pointing at main_arena/allocator state or a saved return address are common variants. Because the primitive gives you an allocation whose user data begins exactly at target, the immediately following program write into that chunk is your arbitrary write.

Note: Off-by-one arithmetic sinks more House of Force attempts than any missing leak. The request that moves the top must account for the 2*SIZE_SZ header that precedes user data and for malloc rounding the request up to the alignment boundary — if you forget the header, the reclaimed chunk lands 0x10 bytes off the target. Compute the distance against the *user-data* address you actually want, and verify in gdb that the top pointer sits at target - 0x10 after the first malloc, before taking the second.

Opsec: House of Force is dead on glibc >= 2.29: the ‘corrupted top size’ check aborts the moment the oversized wilderness is used, so an exploit written for 2.27 will crash loudly on 2.31+. Fingerprint the libc first. If the target is modern, reach for a technique that does not depend on an absurd top size — the top-chunk *shrink* used by House of Orange, or tcache/large-bin approaches, rather than the top-chunk *grow* of House of Force.

Detection and Defense

The classic technique is already defended by modern glibc; the broader defenses target the overflow that reaches the wilderness and the predictability the distance calculation relies on:

  • Run glibc >= 2.29 — the top-chunk size sanity check (“malloc(): corrupted top size”) turns the entire technique into an immediate abort; keeping libc current is the single most effective mitigation.
  • Eliminate the linear overflow — bounds-checked copies and __builtin_object_size/_FORTIFY_SOURCE fortified string functions stop the overwrite of the top header at its source.
  • PIE + ASLR — House of Force needs both the top-chunk address and the target address; randomising the heap and image forces a leak for each and removes static targets.
  • Full RELRO — removes the GOT as a convenient destination for the arbitrary allocation, pushing the attacker toward harder targets.
  • Sanitizers / hardened allocators — ASan detects the heap overflow deterministically in CI; allocators that keep the top-chunk metadata out of band deny the in-line size field the technique overwrites.

Real-World Impact

House of Force was, for most of the 2010s, one of the first heap primitives taught and one of the most reliable in CTF pwn, precisely because a single controlled size request converts a top-chunk overflow into an arbitrary write with no free-list juggling. Its retirement is one of the most cited examples of allocator hardening ending a technique cleanly: the glibc 2.29 top-size check is frequently referenced in write-ups as the moment the ‘grow the wilderness’ approach stopped working, redirecting exploitation toward the top-chunk *shrink*, unsorted/large-bin attacks, and FSOP-based routes. The underlying bug it consumes — a heap buffer overflow, CWE-122 — remains one of the most common memory-safety weaknesses in C/C++ software, which is why the technique is still worth understanding even where glibc has closed it.

Conclusion

House of Force turns one overflow of the top-chunk size into an allocation at an arbitrary address: set the wilderness size to all-ones so no request is too large, request the exact distance to walk the top pointer to your target, and let the next malloc return a chunk there. It is elegant and, on glibc <= 2.28, devastating — but glibc 2.29’s top-size sanity check retired the classic form by making the technique’s precondition the trigger for an abort. The lesson cuts both ways: a linear overflow into allocator metadata is catastrophic, and a single well-placed invariant check can close an entire class of it.

You Might Also Like

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

Comments

Copied title and URL