ShiftCrops つれづれなる備忘録

CTF関連の事やその他諸々

House of Rabbit - Heap exploitation technique bypassing ASLR - [en]

In this article, I will introduce the technique of Heap Exploit newly formed this time.

With this technique, heap address leaks required by the general Heap Exploit method are unnecessary. It is realized to return an arbitrary address with malloc on it, and it is possible to write data to that area.

Although it requires quite a complicated work, I think whether it is quite general versatility if we arrange even the conditions.

This article was originally written in Japanese. If the expression is incorrect, please read the original article. shift-crops.hatenablog.com

Powered by google Translate

Attack Overview

I will explain the prospect of attack method.

In a nutshell, this technique is an attack that connects chunks with very large sizes to largebins.
By maximizing the size of the fake chunk prepared for the known address, it is possible to return an arbitrary address in malloc by going round the address space.

Some people may have thought of House of force as a way of securing space from arbitrary addresses by rewriting size to a huge value. With this existing method, attacks are made possible by making the top chunk size very large. However, if you wish to obtain an absolute address with varying offset from the heap area with malloc, you need to obtain the address of the heap beforehand and generate an exploit on House of force.

In the proposed House of Rabbit, we place fake chunks of very large sizes at known addresses. Therefore, it is possible to attack with the same code regardless of the address to which the heap is mapped. In other words, there is no need to specify the address of the heap.
(When using a library function such as libc as an attack, of course, it is necessary to leak the base address of the shared library)

Actually the name is not decided yet, but for the time being it is temporarily set as ‘House of Rabbit’. It seems that names that are too far from the method are confused, but rabbits do not seem to get bad from the image “Jumping to any address” of this method.

Well, here are the features of this method and the constraints on attacks.

Features

  • Any address can be returned in malloc
    • You can also acquire addresses lower than the area that can be operated by the user
  • It is unnecessary to specify the address where the heap area is located

Constraints

  • You can call any size malloc, and free
  • You can freely write 0x20 bytes or more for known addresses
  • There is a vulnerability that can rewrite fd of fastbins
    • fastbin dup
    • Use After Free

Tested environment

Attack method

This method can be roughly divided into three stages.
Put fake chunks (FC) at known addresses and connect FC to fastbins using vulnerability that can rewrite fastbins' fd. After that, I will connect FC in the order of unsorted bins, largebins.

PoC is on the link below. github.com

I place FC at a known address and tamper the fd of the chunk connected to fastbins to the address of FC. Fastbin dup, Use After Free, etc. can be considered as a technique for tampering, but I do not write it here as these vulnerabilities depend on the application.

The size of FC is 0xfffffffffffffff1, and the size of nextChunk of FC (it is located 0x10 byte before FC by going around the address space) is 0x11. By doing so, the next next in FC becomes FC again, so PREV_INUSE is set for each other. This will be required later when you reconnect FC to unsorted bins.

        // 3. Make fake_chunk on .bss
        printf("3. Make fake_chunk on .bss\n");
        gbuf[1] = 0x11; 
        gbuf[3] = 0xfffffffffffffff1;   
        printf( "  fake_chunk1 (size : 0x%lx) is at %p\n"
                "  fake_chunk2 (size : 0x%lx) is at %p\n\n"
                , gbuf[3], &gbuf[2], gbuf[1], &gbuf[0]);


        // VULNERABILITY
        // use after free or fastbins dup etc...
        fake = &gbuf[2];
        printf( "VULNERABILITY (e.g. UAF)\n"
                "  *fast = %p\n"
                , fake);
        *(unsigned long**)fast = fake;
        printf("  fastbins list : [%p, %p, %p]\n\n", fast-0x10, fake, *(void **)(fake+0x10));

The status of FC is as follows.

gdb-peda$ x/4gx &gbuf
0x602080 <gbuf>:        0x0000000000000000      0x0000000000000011
0x602090 <gbuf+16>:     0x0000000000000000      0xfffffffffffffff1

So far, the state of heap is as follows. f:id:shift_crops:20170915212335p:plain

In PoC, an area where the chunk size is 0x20 bytes is acquired and it is free, so it is connected to fastbin[0]. And as a result of tampering with fd, you can see that 0x602090 which is surely arranging the FC is connected. At this time, since size error has come out because only chunks of size 0x20 byte should be connected to fastbin[0] originally, this behavior is correct in this exploit.

Next, call malloc_consolidate () to merge all chunks connected to fastbins with the previous and next released chunks. Normally fastbins keeps the size as it is without being merged with the released chunks before and after. However, if it becomes too much, fragmentation progresses, it becomes difficult to secure large chunks, or memory usage efficiency gets worse. Therefore, malloc_consolidate is called at a certain timing to eliminate fragmentation.

There are several timings when malloc_consolidate is called. Timing that can be easily invoked by the user’s operation is acquisition of chunks of a size classified as largebin exceeding 0x400 bytes and freeing of chunks whose total size of results merged before and after exceeds 0x10000 bytes. Although the former seems easier, this is not a realistic method because of size checking when reconnecting to largebins described later. Let’s adopt a method to free chunks with merged results exceeding 0x10000 bytes. Simply obtaining a chunk of size larger than 0x10000 bytes in advance and freeing it at this timing, this time we decided to free smallbin located just before the top chunk. There is no problem if the size of top chunk well exceeds 0x10000 byte.

Here is a part of the processing of malloc_consolidate.

4486              if (nextchunk != av->top) {

4495               first_unsorted = unsorted_bin->fd;
4496               unsorted_bin->fd = p;
4497               first_unsorted->bk = p;
4498   
4499               if (!in_smallbin_range (size)) {
4500                 p->fd_nextsize = NULL;
4501                 p->bk_nextsize = NULL;
4502               }
4503   
4504               set_head(p, size | PREV_INUSE);
4505               p->bk = unsorted_bin;
4506               p->fd = first_unsorted;
4507               set_foot(p, size);
4508             }

If the next chunk of the target chunk of consolidate is not top, it will be connected to unsorted bins after consolidate. The point to note here is that there is no size check in this series of processes. It is unavoidable due to the specification of consolidate, but FC of wrong size connected to fastbins is also connected to unsorted bins by this process.

The next chunk of FC prepared earlier is located at 0x602080, this is not top. Also, since we set PREV_INUSE to each other, we can avoid unnecessary consolidate execution in this process.

The state of the heap when malloc_consolidate is finished is as follows. f:id:shift_crops:20170915222608p:plain

You can see that the prepared FC is connected to the bottom unsportbin.

When you call malloc, if there is no chunk of a size suitable for the corresponding bins or unsorted bins, reattach the chunks to smallbins or largebins that are connected to unsorted bins. What is important at this time is that the size of the chunk connected to unsorted bins is checked to see if it is less than system_mem recorded in arena.

Here is the part checking the size of _int_malloc function.

3723          if (__builtin_expect (chunksize_nomask (victim) <= 2 * SIZE_SZ, 0)
3724             || __builtin_expect (chunksize_nomask (victim)
3725                      > av->system_mem, 0))
3726           malloc_printerr (check_action, "malloc(): memory corruption",
3727                    chunk2mem (victim), av);

If you get stuck with the conditions here, the program ends with an error at this point.

By the way, system_mem is an element that holds the size of the acquired heap area. It does not include the size of the area acquired by mmap.

gdb-peda$ p main_arena 
$1 = {
  mutex = 0x0, 
  flags = 0x1, 
  fastbinsY = {0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, 
  top = 0x603410, 
  last_remainder = 0x0, 
  bins = {0x7ffff7dd1b78 <main_arena+88>, 0x7ffff7dd1b78 <main_arena+88>, 0x7ffff7dd1b88 <main_arena+104>, 
    - 省略 -
    0x7ffff7dd21a8 <main_arena+1672>...}, 
  binmap = {0x0, 0x0, 0x0, 0x0}, 
  next = 0x7ffff7dd1b20 <main_arena>, 
  next_free = 0x0, 
  attached_threads = 0x1, 
  system_mem = 0x21000, 
  max_system_mem = 0x21000
}

In the initial state, this value is 0x21000, which is very far from 0xfffffffffffffff0. Since the address of libc is unknown, it is impossible to tamper with system_mem. Therefore, by overwriting the size of FC instead of system_mem, we try to avoid this size check.

However, the size to rewrite is limited. In order to return an arbitrary address later as malloc, it is necessary to assign it from this FC when malloc of very large size is executed. Therefore, FC MUST adjust the size to be connected to largebin[126] which is the largest index of largebins.

The minimum size of the chunk connected to largebin[126] is 0xa00000 byte. Even if you rewrite the size to 0xa00001, it still greatly exceeds system_mem. But here you notice that it is impossible to set system_mem to 0xfffffffffffffff0, but it can be set to a value slightly larger than 0xa00000.

As for how to do it, simply obtain 0xa00000 byte with malloc. However, when trying to acquire such a large size, the system tries to get it by mmap, not from the normal heap area. In this case, system_mem does not increase.

So once we freeze the large size chunk we got in mmap and then get it again. Then, we will use the behavior that it is acquired in the heap area this time. By doing so, you can see that system_mem exceeds 0xa00000.

gdb-peda$ p main_arena 
$2 = {
  mutex = 0x0, 
    - 省略 -
  attached_threads = 0x1, 
  system_mem = 0xa21000, 
  max_system_mem = 0xa21000
}

We recommend that you do this operation in an initial state that does not tamper the link. If you attempt to do this at the time of exploit so far, you will not pass the size check.

        // 1. Make 'av->system_mem > 0xa00000'
        printf("1. Make 'av->system_mem > 0xa00000'\n");
        p = malloc(0xa00000);
        printf("  Allocate 0xa00000 byte by mmap at %p, and free.\n", p);
        free(p);

        p = malloc(0xa00000);
        printf("  Allocate 0xa00000 byte in heap at %p, and free.\n", p);
        free(p);
        printf("  Then, the value of 'av->system_mem' became larger than 0xa00000.\n\n");

Suppose that exploit is executed so far with system_mem expanded in advance. Let’s do malloc of large size and reconnect FC to largebin[126].

       // 5. Link unsorted bins to appropriate list
        printf( "5. Link unsorted bins to appropriate list\n"
                "  Rewrite fake_chunk1's size to 0xa0001 to bypass 'size < av->system_mem' check.\n");
        gbuf[3] = 0xa00001;
        malloc(0xa00000);

f:id:shift_crops:20170915230847p:plain

You can see that FC with size 0xa00000 was connected to largebin[126].

By the way, this is the reason why we chose not to acquire chunks larger than 0x400 bytes as the timing to call malloc_consolidate earlier. In malloc, malloc_consolidate is followed by reattaching to smallbins or largebins. In other words, since processing to reconnect is done without having time to rewrite the FC size, it is impossible to pass the size check.

Get arbitrary address with malloc

Rewrite FC again and set size back to 0xfffffffffffffff1. This means that chunks of size that go around the address space are connected to largebins.

From here it is the same way as House of force. Calculate the offset from the FC to the target area and subtract the header size (0x20) for 2 chunks. Execute malloc with its size as an argument. Next, if you malloc by an arbitrary size, the target address will be returned.

        // 6. Overwrite targer variable
        printf( "6. Overwrite targer variable on .data\n"
                "  target is at %p\n"
                "  Before : %s\n"
                , &target, target);

        malloc((void*)&target-(void*)(gbuf+2)-0x20);
        victim = malloc(0x10);

Let ’s read and write from that address as you like.

 % ./house_of_rabbit                                                                                                                    
This is PoC of House of Rabbit
This technique bypassing Heap ASLR without leaking address, and make it possible to overwrite a variable located at an arbitary address.
Jump like a rabbit and get an accurate address by malloc! :)

- 省略 -

6. Overwrite targer variable on .data
  target is at 0x602050
  Before : Hello, World!
  Allocate 0x10 byte at 0x602050, and overwrite.
  After  : Hacked!!

Related Links