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

the House of Orange

2026-06-144 min readheap pwn glibc

Every technique so far assumed you could free. The House of Orange is the answer to the cruel challenge that gives you only malloc and an overflow — no free anywhere. It's the capstone because it stitches the whole series together: the top chunk (Part 1), the unsorted bin (Part 5), and a detour into file-stream exploitation (FSOP) for the kill.

target./orange
arch
amd64-64-little
libc
2.23 (classic; 2.24's vtable check changed the FILE half)
technique
forge top size -> sysmalloc frees top -> unsorted bin attack -> fake FILE
needs
NO free() — that's the entire point

step 1 — make malloc free the top chunk for you#

When a request is too big for the top chunk, malloc calls sysmalloc to grow the heap. And sysmalloc, rather than waste the old wilderness, frees the old top chunk into the unsorted bin. That's a free you didn't call.

To trigger it deliberately: overflow the top chunk's size down to a small, valid value, then request something larger than it.

top chunk size forged down — PREV_INUSE set, ends on a page boundaryhex
00000000  41 41 41 41 41 41 41 41  01 0c 00 00 00 00 00 00  |AAAAAAAA........|

The forged size (0xc01 here) has to satisfy sysmalloc's bookkeeping: bigger than the minimum chunk, smaller than your upcoming request, PREV_INUSE set, and — the fiddly one — the old top must end on a page boundary so the new heap lines up. Get it right and one oversized malloc files the old top chunk into the unsorted bin.

you just got a chunk and a leak for free

That freed top chunk now has fd/bk pointing into libc (the unsorted bin head). If you can read it back, that's your libc base — the leak half of the exploit, with no info-leak bug required. The House of Orange is as much a leak primitive as an RCE one.

step 2 — unsorted bin attack into _IO_list_all#

Now you have a freed unsorted chunk, so you're back on familiar ground: the unsorted bin attack. Overwrite the chunk's bk so the next allocation writes the unsorted-bin pointer over _IO_list_all — the head of glibc's linked list of open FILE streams.

The same overflow shapes the chunk's data into a fake FILE struct (an _IO_FILE_plus). The unsorted bin attack leaves _IO_list_all pointing at the main arena, but you arrange the chunk so the list walk lands on your fake FILE (the classic trick: a fake file sized into the 0x60 smallbin so bin[4] — reachable from the arena — points at it).

step 3 — the vtable call#

The trap springs when glibc next flushes its streams — on exit, or when the next corrupted malloc aborts. _IO_flush_all_lockp walks _IO_list_all and, for each stream that needs flushing, calls a function through the FILE's vtable:

glibc — _IO_flush_all_lockp, roughlyc
1
2
3
for (fp = _IO_list_all; fp; fp = fp->_chain)
    if ( /* fp has buffered output */ )
        _IO_OVERFLOW (fp, EOF);   // == (*fp->vtable->__overflow)(fp, EOF)

Forge fp->vtable to point at memory you control, put system where __overflow is read, and arrange fp to begin with "/bin/sh" (it's passed as the first argument). The flush calls system("/bin/sh").

orange.py — the shape of itpython
from pwn import *
io = process("./orange"); libc = ELF("./libc.so.6", checksec=False)

overflow_top(p64(0xc01))                 # forge top size, page-aligned end
huge_alloc()                             # sysmalloc frees old top -> unsorted bin
libc.address = leak_unsorted() - 0x3c4b78

fake = make_fake_file(libc)              # _chain, _IO_write_*, vtable -> system
overflow_into_unsorted(bk=libc.sym["_IO_list_all"] - 0x10, file=fake)
trigger_flush()                          # malloc error / exit -> vtable call -> shell
the shell

The next allocation errors out, glibc tries to flush streams, walks the forged _IO_list_all, and calls into your fake vtable:

txt
segfault{n0_fr33_n0_pr0bl3m}

modern status#

The classic is ≤ 2.23. glibc 2.24 added the check that reshaped FSOP forever:

glibc 2.24 — genops.cdiff
+  /* a vtable pointer must point inside the __libc_IO_vtables section */
+  IO_validate_vtable (fp->vtable);

You can no longer point the vtable at arbitrary memory. The technique didn't die — it mutated: aim the vtable at a legitimate glibc vtable whose function does something useful (_IO_str_overflow via _IO_str_jumps), which is the modern House of Orange. Later libc (≥ 2.34, hooks gone) pushed everyone further into pure-FSOP families like the House of Apple — but they all begin exactly here: the file-stream list is reachable, and glibc calls through it on the way out.

That closes the series. From a chunk's size field to a forged FILE vtable — nine parts, and every one of them is the same move: malloc trusts metadata, so lie to it.