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
Walk me through it- Step 1Identify the OOB write in the reroute commandThe binary has a
reroute <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, finishLearn 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 the Heartbleed bug (reading beyond a buffer in OpenSSL) 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 2Leak PIE base and heap addressUse the
replay <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)}") EOFLearn 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 3Overwrite the finish callback at heap offset +0x430Using the OOB write via
reroutewith 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()) EOFLearn 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
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.