Pizza Router picoCTF 2026 Solution

Published: March 20, 2026

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.

Download router and the map files (city1.map, city2.map, city3.map).

Run the binary and understand what input format it expects.

bash
chmod +x router
bash
./router city1.map
  1. Step 1Identify the OOB write in the reroute command
    The 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.
    bash
    chmod +x router
    bash
    ./router city1.map
    bash
    # Commands available: route, reroute, replay, receipt, dispatch, finish
    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 reroute command accepts an integer index and uses it without bounds checking. Concretely, suppose route_list sits at heap address 0x55a0c0 and the C code writes to route_list[idx] where idx is a 64-bit signed integer the user supplied. Pass idx = -16:

    • The compiler emits something like mov [rax + rdi*8], rsi, where rdi holds the index.
    • -16 in two's complement is 0xFFFFFFFFFFFFFFF0. Multiplied by 8 (entry size), it's 0xFFFFFFFFFFFFFF80, i.e. -128.
    • Adding that to the base 0x55a0c0 wraps below it: the store lands at 0x55a040, which is the previous heap allocation.

    That previous allocation is the finish callback 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.

  2. Step 2Leak PIE base and heap address
    Use the replay <id> command to leak a binary address at heap offset +0x2260 (PIE base). Use the receipt <id> command to leak a heap pointer. Compute PIE base from the leaked address.
    python
    python3 << '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)}")
    EOF
    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 replay command 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:

    1. Break on route handling so the heap structure has been allocated. Note the base from info proc mappings or p $rebase(0).
    2. Dump the heap region: x/256gx $heap_base (where $heap_base is the address printed when an early malloc returned).
    3. 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.
    4. For the finish callback at +0x430: break on dispatch, single-step until you see a call through a heap-stored pointer, and note the offset from route_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.

  3. Step 3Overwrite the finish callback at heap offset +0x430
    Using the OOB write via reroute with a negative index, overwrite the finish callback function pointer stored at heap offset +0x430 with the address of the win function or a one_gadget. Then call dispatch to trigger the overwritten callback.
    python
    python3 << '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())
    EOF
    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 dispatch command that triggers the overwritten finish callback 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 the one_gadget Ruby 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:

    1. Open the binary in Ghidra, find the global named something like route_list, g_routes, or whatever the data section names suggest.
    2. Read its raw address from objdump -s -j .data router (or .bss, depending on initialisation).
    3. 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.
    4. Re-derive the OOB index by computing (target_slot - route_list) / 8 with the base from your heap dump.

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.

Want more picoCTF 2026 writeups?

Useful tools for Binary Exploitation

Related reading

What to try next