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.
- 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:
- an overflow into the top chunk's size field (usually an overflow out of the chunk physically before it),
- a
mallocwith an attacker-controlled size, and - a second
mallocwhose returned pointer you can then write through.
A toy victim that hands you all three:
|
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:
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:
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:
|
the shell
With __free_hook = system and a chunk containing /bin/sh\x00, the next
free() fires system("/bin/sh"):
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:
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.