Description
Plan the fastest pizza drone routes and snag a slice of the flag. Download router plus city1.map, city2.map, and city3.map, then optimize the delivery path.
Setup
Download router and the map files (city1.map, city2.map, city3.map).
Run the binary and understand what input format it expects.
chmod +x router./router city1.mapSolution
Want to try it yourself first?
The guided walkthrough reveals hints one step at a time.
Step 1
Identify the OOB write in the reroute commandObservationI noticed the binary accepts a signed integer index from the user in the reroute command with no bounds check, which suggested that supplying a negative value would walk backward past the route_list array and corrupt adjacent heap allocations.The binary has areroute <id> <new_index>command that stores a signed integer index without bounds checking. A negative index writes out-of-bounds on the heap, allowing you to corrupt adjacent heap metadata and overwrite heap pointers. This is the core vulnerability.bashchmod +x routerbash./router city1.mapbash# Commands available: route, reroute, replay, receipt, dispatch, finishWhat didn't work first
Tried: Try using a positive index with reroute to see if it can reach the finish callback forward in memory.
Positive indices write within the route_list array itself, not into the adjacent heap chunk that holds the finish callback. The callback sits at a lower heap address (allocated before route_list), so only a negative index walks backward into it. Positive values corrupt the route array's own data, which does not help redirect control flow.
Tried: Run the binary under checksec or file and assume it has no PIE because the binary looks small.
Even small binaries compiled with -fpie -pie are fully position-independent. checksec will report 'PIE enabled' and the actual load address changes every run under ASLR. Hardcoding any address from a single GDB session into the exploit will fail on the remote instance because the binary base differs each time.
Learn more
An out-of-bounds (OOB) write occurs when a program writes to memory outside the bounds of its intended buffer. Unlike a stack buffer overflow (which overwrites local variables and return addresses), this challenge's OOB write targets the heap - the dynamic memory region used for
malloc()allocations.The vulnerability here is a signed/unsigned confusion bug: the
reroutecommand accepts an integer index and uses it without bounds checking. Concretely, supposeroute_listsits at heap address0x55a0c0and the C code writes toroute_list[idx]whereidxis a 64-bit signed integer the user supplied. Passidx = -16:- The compiler emits something like
mov [rax + rdi*8], rsi, whererdiholds the index. -16in two's complement is0xFFFFFFFFFFFFFFF0. Multiplied by 8 (entry size), it's0xFFFFFFFFFFFFFF80, i.e. -128.- Adding that to the base
0x55a0c0wraps below it: the store lands at0x55a040, which is the previous heap allocation.
That previous allocation is the
finishcallback structure. So a negative index lets you stomp on adjacent heap metadata or function pointers before the route array - exactly what we exploit below.This class of vulnerability is common in real-world software. Notable examples include CVE-2021-3156 (a heap-based buffer overflow in sudo that uses an off-by-one write to corrupt adjacent memory) and many CVEs in network protocol parsers. The lack of bounds checking is particularly dangerous in C/C++ where the language provides no automatic protection - unlike Python, Rust, or Java which raise exceptions or refuse to compile unsafe accesses.
- The compiler emits something like
Step 2
Leak PIE base and heap addressObservationI noticed the binary is compiled with PIE enabled, meaning all addresses are randomized at runtime, which suggested I needed a memory disclosure primitive from the replay and receipt commands to obtain a live binary pointer before attempting any overwrites.Use thereplay <id>command to leak a binary address at heap offset +0x2260 (PIE base). Use thereceipt <id>command to leak a heap pointer. Compute PIE base from the leaked address.pythonpython3 << 'EOF' from pwn import * p = remote("<HOST>", <PORT_FROM_INSTANCE>) # or: p = process(["./router", "city1.map"]) # Trigger route allocation p.sendlineafter(b"> ", b"route 0 1") # create a route entry # Leak PIE base via replay command (reads heap + 0x2260) p.sendlineafter(b"> ", b"replay 0") leak_data = p.recvline() pie_leak = int(leak_data.split()[-1], 16) pie_base = pie_leak - 0x2260 # adjust offset from binary analysis log.info(f"PIE base: {hex(pie_base)}") # Leak heap pointer via receipt command p.sendlineafter(b"> ", b"receipt 0") heap_data = p.recvline() heap_leak = int(heap_data.split()[-1], 16) heap_base = heap_leak - 0x??? # adjust offset log.info(f"Heap base: {hex(heap_base)}") EOFWhat didn't work first
Tried: Use strings or objdump to find a hardcoded PIE address and skip the replay leak entirely.
PIE means the binary is loaded at a random base each run - no address found in the binary's on-disk bytes matches the runtime address. strings and objdump show file-relative offsets, not live virtual addresses. Without calling replay in the session to get a runtime pointer, you cannot compute the actual pie_base for that run.
Tried: Assume the heap offset is always +0x2260 and skip re-deriving it on the actual binary.
The +0x2260 offset is specific to this compiled build of router and the exact heap allocation sequence it uses. A recompile or a different libc version changes the heap layout, shifting the PIE pointer to a different slot. The correct approach is to dump heap contents in GDB, scan for a value in the text segment, and compute the offset from that session's route_list base.
Learn more
PIE (Position-Independent Executable) is a security feature that randomises the base address where the binary is loaded in memory at runtime (ASLR - Address Space Layout Randomisation applied to the executable itself). Without a leak, an attacker cannot predict the addresses of functions, gadgets, or data structures.
An information leak (or memory disclosure vulnerability) is a bug that causes the program to output memory contents it shouldn't. In this challenge, the
replaycommand reads a value from a specific heap offset and prints it; that value happens to contain a pointer into the binary's code section. Since the binary and heap are loaded at fixed offsets relative to each other (their layout within a process is deterministic even if the base addresses are random), knowing one pointer leaks the randomisation for both.Where the +0x2260 number came from. The offsets in this writeup are specific to this build of
router. Re-derive them on a fresh binary with GDB:- Break on
routehandling so the heap structure has been allocated. Note the base frominfo proc mappingsorp $rebase(0). - Dump the heap region:
x/256gx $heap_base(where$heap_baseis the address printed when an earlymallocreturned). - Spot the slot whose value lies inside the binary's text segment - that's the leakable PIE pointer. Subtract its base address from the value to get the in-binary offset (the constant you hardcode into the exploit), and subtract the heap base from its slot to get +0x2260.
- For the
finishcallback at +0x430: break ondispatch, single-step until you see a call through a heap-stored pointer, and note the offset fromroute_list's base.
The pattern of "leak an address to defeat ASLR, then use it to compute target addresses" is the standard approach in modern exploitation. Most modern exploits require at least one information leak before they can compute reliable ROP chain addresses or overwrite function pointers. For more on the ASLR/PIE pieces see ASLR and PIE bypass for CTF; for the broader heap-corruption playbook see Heap exploitation for CTF.
- Break on
Step 3
Overwrite the finish callback at heap offset +0x430ObservationI noticed the finish callback function pointer is stored on the heap at offset +0x430 relative to route_list, which suggested that the OOB write via a negative reroute index could stomp exactly that slot and redirect the dispatch call to the win function.Using the OOB write viareroutewith a negative index, overwrite thefinishcallback function pointer stored at heap offset +0x430 with the address of the win function or a one_gadget. Then calldispatchto trigger the overwritten callback.pythonpython3 << 'EOF' from pwn import * p = remote("<HOST>", <PORT_FROM_INSTANCE>) # After leaking addresses, compute the target and payload win_addr = pie_base + 0x???? # address of win/print_flag function # Negative index for OOB write - reroute <id> <negative_idx> # The exact offset depends on heap layout analysis p.sendlineafter(b"> ", f"reroute 0 -<OFFSET>".encode()) # Write win_addr bytes into the finish callback slot at +0x430 # Trigger the overwritten callback p.sendlineafter(b"> ", b"dispatch") print(p.recvall()) EOFExpected output
picoCTF{p1zz4_r0ut3r_...}What didn't work first
Tried: Use one_gadget on the router binary itself to find a magic gadget and jump there directly.
one_gadget is designed for libc - it searches for gadgets that call execve with the right register state, which only exist in the C library's execve wrapper chain. The router binary itself has no such self-contained gadget. The correct target is the win or print_flag function inside the binary, whose address you compute by adding its Ghidra-derived in-binary offset to pie_base.
Tried: Compute the negative reroute index from the +0x430 offset alone without accounting for the route_list base.
The index passed to reroute is relative to route_list's runtime heap address, not to the heap's start. The formula is (finish_callback_addr - route_list_addr) / 8, which requires knowing both addresses from a live heap dump. Using just the +0x430 literal as a negative index ignores the heap distance between route_list and the finish structure, landing the write at a completely wrong location.
Learn more
Function pointer overwrites are one of the most powerful heap exploitation primitives. When an attacker can overwrite a function pointer stored on the heap (e.g. a callback, a vtable entry, or a registered handler), the next call to that function executes the attacker's chosen address instead. This redirects control flow without touching the return address, bypassing stack canaries entirely.
The
dispatchcommand that triggers the overwrittenfinishcallback is the equivalent of the "trigger" step in any use-after-free or heap corruption exploit: you first set up the corruption, then trigger the code path that dereferences the corrupted pointer. The window between corruption and trigger is where real-world mitigations like pointer authentication codes (PAC on ARM) or safe unlinking checks would intervene.One-gadget (also called "magic gadget") refers to a specific location in libc that, when jumped to directly, executes
execve("/bin/sh")with the right register/environment conditions. Tools like theone_gadgetRuby gem enumerate these addresses in a given libc. Unlike a full ROP chain, a one-gadget lets you win with a single pointer overwrite, if the environmental constraints at the call site are met. When constraints don't match, you fall back to a real ROP chain - see ROP chains without libc.Porting this exploit to a new build. The two hardcoded numbers (
0x2260,0x430, plus the win-function offset) will drift if the binary is recompiled. The fix is mechanical:- Open the binary in Ghidra, find the global named something like
route_list,g_routes, or whatever the data section names suggest. - Read its raw address from
objdump -s -j .data router(or.bss, depending on initialisation). - Subtract the binary's base from the leaked pointer to recover the live PIE base, then add the win-function offset (also from Ghidra) to get the destination address.
- Re-derive the OOB index by computing
(target_slot - route_list) / 8with the base from your heap dump.
- Open the binary in Ghidra, find the global named something like
Interactive tools
- pwntools Payload BuilderPack integers into little-endian bytes (p32 / p64), unpack bytes back to integers, and build flat ROP payloads with offset-based insertion.
- Cyclic Pattern GeneratorGenerate de Bruijn cyclic patterns and find buffer overflow offsets. The browser equivalent of pwntools cyclic and cyclic_find.
Flag
Reveal flag
picoCTF{p1zz4_r0ut3r_...}
Pizza Router is a heap exploitation challenge. The `reroute` command uses a signed index without bounds checking - a negative value performs an OOB write on the heap. Leak PIE base via `replay` (offset +0x2260) and heap via `receipt`, then overwrite the `finish` callback at offset +0x430 and call `dispatch` to win.