PIE TIME

Published: April 2, 2025

Description

A PIE-protected binary leaks the address of `main` each time you connect. Use that leak to compute the absolute address of `win` and jump there instead of returning to `main`.

Fetch both the binary and its source so you can inspect the control flow (`win` simply prints the flag).

Connect to `nc rescued-float.picoctf.net 59193` and note the leaked address of `main`.

Use objdump or radare2 locally to record the offsets of `main` and `win` inside the binary.

wget https://challenge-files.picoctf.net/c_rescued_float/2736a730340dbe9969fe3104da0cca0c60eddaf1fedb0e220b5df5a3f3cf015f/vuln.c
wget https://challenge-files.picoctf.net/c_rescued_float/2736a730340dbe9969fe3104da0cca0c60eddaf1fedb0e220b5df5a3f3cf015f/vuln
objdump -D vuln | grep -E "<win>|<main>"

Solution

The Buffer Overflow and Binary Exploitation guide covers PIE and ASLR bypass (the technique here) - leak an address, compute the base, then redirect execution.
  1. Step 1Derive the PIE base
    Write down the leaked `main` address from the remote service. Subtract the local `main` offset (0x133d) to recover the PIE base address for that run.
    pie_base = leaked_main - 0x133d
    Learn more

    Position Independent Executables (PIE) are binaries compiled with -fPIE -pie so that every instruction and data reference uses relative addressing. At load time, the OS kernel maps the binary to a random base address chosen by ASLR (Address Space Layout Randomization). Every subsequent address inside the binary is that base plus the static offset from the compiled binary - so main's runtime address equals pie_base + 0x133d.

    By inverting this arithmetic - subtracting the known offset of main from its leaked runtime address - you recover the pie_base for that specific execution. This value remains constant throughout the process lifetime, so once known, you can compute the absolute address of any other symbol. This is exactly how return-oriented programming (ROP) and other advanced exploits work after obtaining a single leak.

    objdump -D disassembles all sections of the binary and prints symbol addresses as offsets from zero (since the binary isn't loaded yet). readelf -s and nm provide cleaner symbol table output. The key insight is that these offsets are fixed at compile time - ASLR only randomizes the base, not the relative layout.

  2. Step 2Compute the win address
    Add the known `win` offset (0x12a7) to the PIE base to get the absolute address of `win`. A three-line Python helper that does `win_address = leaked_main - main_offset + win_offset` keeps the math simple.
    win_address = leaked_main - 0x133d + 0x12a7
    Learn more

    The address arithmetic win_address = leaked_main - main_offset + win_offset is the fundamental formula for all PIE-defeat exploits. It generalizes to any two symbols in the binary: knowing one runtime address and both static offsets lets you compute any other runtime address. CTF players often write a small Python script using pwntools (from pwn import *) which automates connecting to the service, parsing the leaked address, computing the target address, and sending the exploit.

    Pwntools' ELF class can parse the binary automatically: elf = ELF('./vuln'); main_offset = elf.symbols['main']; win_offset = elf.symbols['win']. This avoids manual offset extraction from objdump and keeps the exploit script portable across different binary versions. The context.arch and context.log_level settings further simplify address packing and debugging output.

    Real-world exploitation frequently involves leaking a libc address (via a puts call or format string) rather than a binary address, computing the libc base, then jumping to system('/bin/sh') or using a ROP gadget chain. The arithmetic is identical - only the target binary (libc vs. the vuln binary) changes.

  3. Step 3Send the target address
    Reconnect (or keep the connection open), paste the computed `0x...` value when prompted, and the binary jumps straight into `win`, printing the flag.
    Learn more

    The mechanism that lets you "send an address" and have the binary jump there is almost always a stack buffer overflow. The vulnerable program reads more input than the buffer can hold, overwriting the saved return address on the stack. When the current function executes its ret instruction, it pops your supplied address into the instruction pointer and execution continues from there.

    The win function pattern - a function that prints the flag but is never called by normal program flow - is a classic CTF teaching device. In real-world binary exploitation, there is no win function; attackers typically chain ROP gadgets to call execve('/bin/sh', NULL, NULL) or use a one-gadget (a single libc address that directly spawns a shell under the right register conditions).

    Keeping the connection alive (rather than reconnecting) is important because ASLR generates a new random base on each process execution. If the service forks (a common CTF server pattern), the child inherits the parent's address space layout, so the leak from one connection is valid for subsequent requests to the same child process. New connections that spawn new processes get fresh ASLR randomization.

Flag

picoCTF{b4s1c_p051t10n_1nd3p3nd3nc3_31cc...}

Keep one terminal open with nc to grab the current leak and a second to run the helper script so the values stay in sync.

Want more picoCTF 2025 writeups?

Useful tools for Binary Exploitation

Related reading

Do these first

What to try next