Skip to main content

Pizza Router picoCTF 2026 Solution

A binary exploitation challenge involving unsafe memory access in a routing program.

Published: March 20, 2026Updated: September 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

Solution

Want to try it yourself first?

The guided walkthrough reveals hints one step at a time.

Walk me through it
  1. Step 1Identify the OOB write in the reroute command
    Observation
    The reroute command takes a signed integer index with no bounds check. A negative value walks backward past the route list and into the neighboring heap allocations.
    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
    What didn't work first

    Tried: Try using a positive index with reroute to see if it can reach the finish callback forward in memory.

    A positive index writes inside the route list itself, never into the chunk holding the callback. That chunk was allocated earlier and sits at a lower address, so only a negative index reaches it. Positive values corrupt route data and change nothing about control flow.

    Tried: Run the binary under checksec or file and assume it has no PIE because the binary looks small.

    Even a small binary built with PIE is fully position-independent, and checksec says so. The load address changes every run under ASLR, so an address copied from one GDB session fails against the remote instance, where the base is different.

    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 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.

  2. Step 2Leak PIE base and heap address
    Observation
    The binary is PIE, so every address is randomized at runtime. Get a live binary pointer out of the replay and receipt commands before attempting any overwrite.
    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
    What didn't work first

    Tried: Use strings or objdump to find a hardcoded PIE address and skip the replay leak entirely.

    PIE loads the binary at a random base each run, so no address in the file matches a runtime one. strings and objdump report file-relative offsets, not live addresses. Without a runtime pointer from replay, there is no base to compute.

    Tried: Assume the heap offset is always +0x2260 and skip re-deriving it on the actual binary.

    That offset belongs to this exact build and its allocation sequence. A recompile or a different libc rearranges the heap and moves the pointer to another slot. Dump the heap in GDB, find a value inside the text segment, and compute the offset from that session's route list.

    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 walkthrough 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
    Observation
    The finish callback lives on the heap at a fixed offset from the route list. A negative index puts the out-of-bounds write on exactly that slot, and the dispatch call goes wherever you point it.
    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"> ", b"reroute 0 -<OFFSET>")
    # Write win_addr bytes into the finish callback slot at +0x430
    
    # Trigger the overwritten callback
    p.sendlineafter(b"> ", b"dispatch")
    print(p.recvall())
    EOF

    Expected output

    picoCTF{...}
    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 searches libc for sequences that reach execve with the right register state, and those live in the C library's wrapper chain. This binary has nothing comparable. Target its own win or print_flag function, adding the offset Ghidra gives you to the recovered base.

    Tried: Compute the negative reroute index from the +0x430 offset alone without accounting for the route_list base.

    The index is relative to the route list's runtime address, not to the start of the heap. Compute it as the distance between the callback and the route list divided by 8, which needs both addresses from a live dump. Using the raw structure offset ignores the heap distance and lands the write somewhere else entirely.

    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 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.
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{...}

This flag could not be verified. The binary reads /flag.txt from the live instance, so there is no offline derivation, and no independent solve publishes the value. The site's earlier value was a guess and has been withdrawn. Run the exploit against your own instance to get yours.

Key takeaway

Signed indexing is a classic C bug: allow a signed integer as a subscript with no bounds check and a negative value produces a negative offset from the base pointer, walking backward into whatever was allocated before. Pair that with a disclosure primitive that defeats ASLR and an attacker finds a function pointer on the heap, overwrites it with one write, and takes execution the next time the program calls through it. Leak then corrupt is the shape of modern heap exploitation, from tcache attacks to vtable overwrites to GOT hijacks.

Related reading

Useful tools for Binary Exploitation

Where to go next