Tcache机制及漏洞利用方法

0x00 写在前面

Tcache机制是在libc-2.26中引入的一个新的堆管理机制。掐指一算,libc-2.26发布距今应该也有一两年了。由于各种不可控因素,到近期才将其提上日程。

0x01 What's New

首先得介绍在libc-2.26中新引进的tcache_perthread_structtcache_entry两个结构体。

  • tcache_perthread_struct
#define TCACHE_MAX_BINS 64
typedef struct tcache_perthread_struct{
    char counts[TCACHE_MAX_BINS];
    tcache_entry *entries[TCACHE_MAX_BINS];
}tcache_perthread_struct;
  • tcache_entry
typedef struct tcache_entry{
    struct tcache_entry *next;
}tcache_entry;

从源码中不难看出,tcache_perthread_structTcache机制中起管理作用,在默认情况下,sizeof(tcache_perthread_struct) == 0x240,即可以对低于0x400大小的堆块进行管理。其中tcache_pthread_struct.counts[i]64bit系统中对应大小为8 * i堆块的Tcachebin的数量,最大可为7tcache_pthread_struct.tcache_entry[i]则指向该大小对应的第一个Tcachebinfd的位置。

  • 举个栗子
    为对这两个结构体有直观的印象,下面的例子中释放了2个大小为0x201个大小为0x20的堆块。
    堆块分布

    其中第一个0x250的堆块是在第一次申请内存时,会分配一个空间用于存放tcache_pthread_struct(0x240 + 堆头 == 0x250)
    tcache_pthread_struct

    上图中则展示了第一个堆块,即tcache_perthread_struct中存放的数据。
    图中红色方框内的数据,即对应结构体中的count,共0x40字节,每字节对应相应大小Tcachebin中的个数。如绿色方框中对应size0x20大小的Tcachebin中有2个空闲堆块,而黄色方框中则对应size0x30大小的Tcachebin中有1个空闲堆块。
    图中蓝色方框内的数据,即对应结构体中的tcache_entry,共0x200字节(0x40 * 8 == 0x200),每一个指针对应相应大小Tcachebin中第一个堆块的入口地址。如绿色箭头对应size0x20大小的Tcachebin的入口地址,二黄色箭头则对应size0x30大小的Tcachebin的入口地址。

0x02 What's Tcachebin

从宏观来看,Tcachebin的各项操作与Fastbin大同小异,如FILO(先进后出)的单循环链表、精确分配(不切割)、free后为防止合并后一个堆块的inuse位不置0等。
但在细节上仍存在些许差异,如Fastbinfd是指向链表中下一个堆块的堆头,而Tcachebinfd则是直接指向链表中下一个堆块的fd。除此之外,在从Tcachebin中申请回内存块时,并没有特定的代码去检验该内存块的大小是否与这条Tcachebin所管理的大小相吻合!
以上两点差异意味着在Tcachebin中利用类似Fastbin Attack的技巧时,不需要再去找到合适的地址伪造size位,不需要再去计算堆头到data区域的偏移,而是指哪儿打哪儿(fd伪造到哪里,之后写的就是哪里)。

  • free
    在该机制中释放大小低于0x400字节的堆块时会首先放入Tcachebin,而不是原来的FastbinUnsortbin。当对应大小的Tcachebin中放满7个空闲堆块后,下一次free的堆块才会放入对应的FastbinUnsortbin中。在放入Tcachebin时会调用tcache_put函数,其代码如下:
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]);
}

总的来说,就是将free的堆块插入Tcachebin的前端,将fd指向前一个堆块,并将对应的tcache_entry指向当前堆块,再将count+1

  • malloc
    相对应与内存释放,内存申请会遇到很多种不同的内存布局情况。在申请大小低于0x400的堆块时,首先会考虑从Tcachebin中去寻找。如上文中提到的,从Tcachebin中取出堆块时的逻辑除了不会检查size位之外,几乎与Fastbin相同,只会进行精准匹配,不会进行切割,在取出时会调用tcache_get函数,其代码入下:
tcache_get(size_t tc_idx)
{
    tcache_entry *e = tcache->entries[tc_idx];
    assert(tc_idx < TCACHE_MAX_BINS);
    assert(tcache->counts[tc_idx] > 0);
    tcache->entries[tc_idx] = e->next;
    --(tcache->counts[tc_idx]);
    return (void*)e;
}

若对应大小的Tcachebin为空,则会从对应大小的bin中去寻找(寻找顺序同之前版本)。在这种情况下,即Tcachebin未满时,却从Fastbin/Smallbin中取出堆块,则会将链上的其他堆块都链入Tcachebin中。其具体算法是首先将Fastbin/Smallbin中取出的堆块指针进行保存,并判断该大小对应的Tcachebin是否未满,若未满则将其之后的堆块按照Fastbin/Smallbin的分配顺序将堆块链入Tcachebin中,直到对应大小的Tcachebin放满或Fastbin/Smallbin的链为空,最后将之前取出的堆块指针返回给用户使用。由于是按照Fastbin/Smallbin的分配顺序将堆块放入Tcachebin中,因此不难判断,最从Tcachebin中申请的堆块顺序是与正常从Fastbin/Smallbin中申请堆块顺序时反向的。

  • 关于将Fastbin/Smallbin中堆块放入Tcachebin中的操作
    针对这个Tcachebin未满,放入堆块的操作,之前在网上看到大部分的描述都是先把大小相同的堆块从Fastbin/Smallbin中放入Tcachebin后再进行分配,但在进行调试的时候发现跟这个描述略有出入,不是很理解,于是通过阅读源码可以非常清楚地看到整个操作的逻辑。
    Fastbin:3594行保存了Fastbin中即将分配的指针,3608-3631行将其后的堆块按Fastbin的申请顺序(FILO)放入Tcachebin3632行将第3594行保存的指针返回给用户使用。
    Smallbin:3652行保存了Smallbin中即将分配的指针,3664-3689行将其后的堆块按Smallbin的申请顺序(FIFO)放入Tcachebin3690行将第3652行保存的指针放回给用户使用。
    下面以Fastbin为例,贴出简化后的代码:
if(nb <= get_max_fast())                                            //申请范围在Fastbin之内
{
   idx = fastbin_index(nb);                                       //找到对应大小的Fastbin索引
   mfastbinptr *fb = &fastbin(av , idx);                 //通过索引找到入口
   victim = *fb;                                                         //注意这里,保存了第一个即将分配堆块的指针
   if(victim != NULL)                                                //如果Fastbin中有堆块
   {
       *fb = victim->fd;                                            //此时fb为即将分配堆块的fd
       ............
#if USE_TCACHE
       ............
       while(tcache->counts[tc_idx] < mp_.tcache_count && (tc_victim = *fb) != NULL)
       {                                                                      //直到Tcachebin放满,或Fastbin取空为止
           *fb = tc_victim->fd;                                  //循环遍历
           ............
           tcache_put(tc_victim , tc_idx);                //将该堆块放入Tcachebin中
       }
#endif
       void *p = chunk2mem(victim);                  //注意这里的victim为最开始保存的即将分配的堆块、
       return p;                                                      //返回给用户使用
   }
}

0x03 How To Pwn

由于这方面的题目接触的不算特别多,目前就只从大佬的博客上了解了一些基础的利用方法,可能以后会遇到再进行更详细和深入的归纳总结吧。

  • Tcache poisoning
    在上一部分中也提到过这种方法。与Fastbin Attack类似,篡改Tcachebin中的fd字段,导致在申请被篡改堆块后的下一个堆块时能够申请到任意地址。与Fastbin相比,Tcachebin中为了得到更高的效率而舍去了安全性,在进行申请时没有对size位进行校验,而且由于Tcachebin中的fd是指向下一个堆块的fd(Fastbinfd是指向下一个堆块的堆头),因此指向的地址即是申请后写数据的地址,不再需要去考虑堆头的偏移。

  • Tcache dup
    这是Tcache机制刚推出的几个版本中,在进行free操作时没有对这个堆块进行一个安全检测而导致可以对同一个堆块进行多次free,那么就会变成一个Tcachebin链上链了两个相同的堆块(我指向我自己),后面也就不用多说了。但值得一提的是在libc-2.29版本中加入了检查机制(源码4201-4216行),会在堆块进行free时检查这个堆块是否已经存在于这条链上,如果存在则会报"free():double free detected in tcache 2"的错误,因此这种直接double free利用方式存在于libc-2.26libc-2.28的版本中。

  • Tcache perthread corruption
    在最开始介绍结构体时提到的tcache_perthread_struct结构体,该结构体size0x250,是管理整个Tcachebin的结构体,如果对这个结构体有写权限,那么可以控制任意大小Tcachebin的入口地址。

  • U2T
    U2TUnsortbin 2 Tcachebin,这种叫法是在一篇文章中看到的,也只看到过一次,主要是配合Off By OneOff By NULL的漏洞,使Unsortbin在合并过程中将中间的Tcachebin合并,从而达到修改fd字段的效果。

你可能感兴趣的:(Tcache机制及漏洞利用方法)