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 Orange is the famous heap technique for the program that never gives you a free(). Where almost every other house begins with the attacker freeing something, House of Orange manufactures a free chunk out of thin air by shrinking the top chunk: it overwrites the wilderness size with a smaller, page-aligned value, then makes an allocation larger than the (now small) top can satisfy. Forced to extend the heap, glibc’s sysmalloc frees the *old* top chunk into the unsorted bin — handing the attacker a free chunk without a single call to free. That free chunk is then leveraged with an unsorted-bin attack to write a libc pointer into _IO_list_all, and a forged _IO_FILE structure turns the next allocator abort into file-stream-oriented programming (FSOP) and code execution.
House of Orange (named for the challenge it debuted on) is the canonical chain that ties allocator internals to the _IO_FILE/FSOP world, and it is a story of layered mitigations. The classic end-to-end chain — top-shrink, unsorted-bin attack into _IO_list_all, fake FILE with a hijacked vtable — works on glibc up to and including 2.23. glibc 2.24 added vtable validation (_IO_vtable_check) that verifies a FILE’s vtable lies within the known __libc_IO_vtables region, breaking the naive vtable overwrite; and the unsorted-bin attack itself was killed around glibc 2.28–2.29 by a check that the unsorted list is consistent (bck->fd == victim). The technique therefore evolved — later FSOP variants (House of Orange with _IO_str_overflow, and ultimately House of Apple) route around the vtable check — but the top-shrink primitive at its core remains a textbook trick.
Attack Prerequisites
House of Orange trades the need for a free primitive for a specific overflow and a lot of glibc-internals knowledge. The classic chain needs:
- A heap overflow into the top chunk’s size field — enough to overwrite the wilderness size with a smaller, carefully chosen value.
- No
free()primitive required — this is the technique’s signature; the free chunk is produced bysysmallocextending the heap. - Control over allocation requests so you can ask for a size larger than the shrunk top, forcing the heap extension that frees the old top.
- A libc leak (often obtained from the freed old-top chunk once it lands in the unsorted bin, whose
fd/bkpoint intomain_arena) to locate_IO_list_alland build the fake FILE. - glibc <= 2.23 for the classic chain; on 2.24+ the vtable check forces an FSOP variant, and on 2.28+ the unsorted-bin-attack write is itself blocked.
How It Works
The first primitive is the top-chunk shrink. When malloc cannot satisfy a request from bins or from the top chunk, it calls sysmalloc to grow the arena. Before extending, sysmalloc frees the *old* top chunk back into the unsorted bin — but only if the old top passes an assertion: its size must be at least MINSIZE, it must have PREV_INUSE set, and critically its end (old_top + old_size) must be page-aligned. House of Orange forges a top size that keeps the end page-aligned while being small enough that the next request overflows it, so the assertion holds and the old top is legitimately freed. The attacker now owns a free chunk in the unsorted bin — plus, because that chunk’s fd/bk are set to main_arena, a ready-made libc leak if the program prints the chunk.
The second primitive is the unsorted-bin attack. A chunk in the unsorted bin stores fd and bk pointers; when malloc later scans the unsorted bin, it unlinks the chunk with the classic operation bck = victim->bk; bin->fd = bck; bck->fd = bin; — writing the address of the unsorted-bin head (a libc address) to victim->bk + 0x10. By overwriting the freed old-top’s bk to &_IO_list_all - 0x10, the attacker makes that scan write a libc pointer into _IO_list_all, the global head of the linked list of open _IO_FILE streams. _IO_list_all now points into the unsorted bin — memory the attacker can shape.
The third primitive is FSOP. When glibc hits a fatal condition — or on normal exit — it walks the _IO_list_all chain and flushes each stream, calling function pointers out of each FILE’s vtable. Because _IO_list_all now points at attacker-controlled memory, the attacker lays out a fake _IO_FILE whose fields satisfy the flush path’s conditions and whose vtable pointer is redirected so that the flush call lands on system (or a one_gadget), with the FILE pointer arranged to double as a "/bin/sh" argument. On glibc <= 2.23 the vtable pointer can be aimed anywhere; glibc 2.24’s _IO_vtable_check constrains the vtable to the legitimate table region, which is why later variants pivot to functions like _IO_str_overflow/_IO_str_finish that live inside the validated vtables and can still be abused. The trigger is often deliberate: corrupting the heap so the *next* malloc aborts causes glibc’s error path to flush streams, firing the forged vtable call.
Vulnerable Code / Setup
The enabling bug is a heap overflow that reaches the top-chunk size, in a program that offers allocation and editing but *no* free. The overflow both shrinks the top and, on a later pass, plants the unsorted-bin bk:
#include <stdlib.h>
#include <string.h>
/* No free() is exposed. Only alloc + edit. The edit overflows into the
top chunk header that sits after the last allocation. */
char *obj;
void setup(void) {
obj = malloc(0x18);
/* BUG: overflow past obj rewrites the top chunk size.
Choose a value that keeps (top + new_size) page-aligned and small,
e.g. 0x0fe1 on a fresh heap, so sysmalloc will free the old top. */
char p[0x28];
memset(p, 'A', 0x18);
*(unsigned long *)(p + 0x18) = 0x0fe1; /* shrunk, page-aligned end */
memcpy(obj, p, 0x20);
/* Request larger than the shrunk top -> sysmalloc frees old top into
the unsorted bin (a free chunk with NO call to free). */
malloc(0x1000);
}
CThe shrink value is not arbitrary: the old top’s end must stay page-aligned or sysmalloc‘s assertion fails. With the old top now in the unsorted bin, a second overflow plants the unsorted-bin-attack bk and forges the fake FILE. Triage confirms the version and whether the classic vtable overwrite applies:
$ checksec --file=./orange
RELRO STACK CANARY NX PIE
Partial Canary found NX enab. No PIE
$ ldd ./orange | grep libc # <=2.23 classic; 2.24+ needs an FSOP variant
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (2.23)
BashWalkthrough / Exploitation
Watch the top-shrink actually produce a freed chunk before building the FSOP stage — the presence of the old top in the unsorted bin is the checkpoint. In gdb with pwndbg:
gdb -q ./orange
pwndbg> run
pwndbg> top_chunk # note top address + size before the overflow
pwndbg> # after overflow + the large malloc:
pwndbg> bins # old top now sits in the unsorted bin
pwndbg> p (void*)&_IO_list_all # target of the unsorted-bin attack
pwndbg> x/32gx <fake_file> # inspect the forged _IO_FILE you build
BashThe chain, in order: leak libc from the freed old-top; overwrite that chunk’s bk to &_IO_list_all - 0x10; forge a fake _IO_FILE in the same region with fields that pass the flush path and a redirected vtable; then trigger an allocator abort so glibc flushes streams through the forged vtable. A pwntools skeleton:
from pwn import *
io = process('./orange')
libc = ELF('./libc.so.6')
def alloc(sz, data=b''): ... # menu wrappers (no free exposed)
def edit(data): ...
# 1) shrink top -> large alloc -> old top freed into unsorted bin
edit(b'A'*0x18 + p64(0x0fe1))
alloc(0x1000)
# 2) read the freed chunk to leak a main_arena/libc address
libc.address = leak_libc() - libc.sym['main_arena'] - 0x60
io_list_all = libc.sym['_IO_list_all']
system = libc.sym['system']
# 3) forge fake FILE: set bk=&_IO_list_all-0x10 (unsorted-bin attack),
# craft _mode/_IO_write_ptr etc. so the flush path is entered, and
# point the vtable so the flush call resolves to system('/bin/sh').
fake = build_fake_file(io_list_all, system, binsh=b'/bin/sh\x00')
edit(fake)
# 4) trigger: next malloc aborts -> glibc flushes streams -> vtable call
alloc(0x60) # error path fires the forged FILE
io.interactive()
PythonThe escalation is the FSOP call itself: with _IO_list_all pointed at the forged FILE and the vtable redirected, the stream-flush that glibc performs on abort/exit calls system with the FILE pointer (arranged to begin with "/bin/sh"), yielding a shell. On glibc 2.24+ the same skeleton is retained but the vtable target is chosen from within the validated vtable region (e.g. abusing _IO_str_overflow), and on 2.28+ the unsorted-bin-attack write must be replaced by another way to control _IO_list_all, since that write primitive is blocked.
Note: The top-shrink is the load-bearing trick and its arithmetic is strict: the forged top size must leave
old_top + sizepage-aligned, orsysmalloc‘s assertion aborts before the old top is freed. On a fresh main-arena heap the classic value is0x0fe1(top ends on the page boundary), but it depends on the current top address — compute it from the livetop_chunkvalue in gdb rather than copying a constant.
Opsec: House of Orange is exquisitely version-bound. The pure vtable-overwrite ending works only up to glibc 2.23; 2.24 adds
_IO_vtable_checkand 2.28/2.29 kill the unsorted-bin-attack write with the ‘corrupted unsorted chunks’ consistency check. Fingerprint the libc first and pick the matching FSOP variant — a 2.23 chain aborts immediately on a 2.27+ target. Confirm each stage in gdb (binsafter the shrink,_IO_list_allafter the unsorted-bin attack) before wiring the trigger.
Detection and Defense
House of Orange chains a top-chunk overflow into allocator and FILE internals, so defenses span the overflow, the allocator checks, and the FSOP surface:
- Keep glibc current — vtable validation (2.24+), the unsorted-bin consistency check (2.28/2.29), and later top-size sanity checks each remove a link of the classic chain.
- Eliminate the top-chunk overflow — fortified copies and bounds checking stop the shrink that starts the whole technique.
- Full RELRO + PIE + ASLR — force a libc leak (the technique needs
_IO_list_allandsystemaddresses) and remove static targets. - Harden the FILE surface — modern glibc’s
_IO_vtable_checkand pointer-guard mangling on some function pointers blunt naive FSOP; do not run pre-2.24 libc on exposed services. - Sanitizers in CI — AddressSanitizer catches the top-chunk heap overflow deterministically, well before it reaches
sysmalloc.
Real-World Impact
House of Orange is one of the most influential heap write-ups in the exploitation canon: it popularised the top-chunk shrink, made the unsorted-bin attack widely understood, and was the on-ramp for a generation of researchers into _IO_FILE/FSOP exploitation. Its mitigations are equally instructive — the vtable check and the unsorted-bin consistency check are frequently cited as the reason later techniques (House of Apple, _IO_str_overflow chains) exist at all. While the exact classic chain is now a historical artifact on modern glibc, the primitives it introduced remain load-bearing in current exploitation, and the FSOP concept it demonstrated is central to defeating hardened, hook-free glibc where __malloc_hook/__free_hook no longer exist. It endures as the definitive lesson that allocator metadata and libc’s I/O structures are one continuous attack surface.
Conclusion
House of Orange is the heap technique for when there is no free: shrink the top chunk so that a large request forces sysmalloc to free the old top into the unsorted bin, leak libc from that chunk, use an unsorted-bin attack to point _IO_list_all at a forged _IO_FILE, and trigger an allocator abort so the stream flush executes your hijacked vtable. The classic end-to-end chain is a glibc-2.23 artifact — vtable validation and the unsorted-bin consistency check retired it — but the top-shrink primitive and the FSOP idea it introduced are permanent fixtures of heap exploitation, and its evolution into modern FSOP is the clearest map of how the allocator and libc’s I/O layer connect.
You Might Also Like
If you found this useful, these related deep-dives cover adjacent techniques and their defenses:



Comments