回顾double free手法
- 在glibc2.27之前,主要是fastbin double free:
- fastbin在free时只会检查现在释放的chunk,是不是开头的chunk,因此可以通过free(C1), free(C2), free(C1)的手法绕过
- 并在在fastbin取出时,会检查size字段是不是属于这个fastbin,因此往往需要伪造一个size
- glibc2.27~glibc2.28,主要是tcache double free
- 相较于fastbin double free,tcache完全没有任何检查,只需要free(C1), free(C1)就可以构造一个环出来
- glibc2.29~glibc2.31,tcache加入了检查机制,如何进行doubel free就是本文的核心
Tcache的设计目的
tcache全称是Thead Cache的意思
在多线程情况下,ptmalloc会遇到抢占的问题,分配区被占用时只能等待或者申请一个非主分配区,效率低下
针对这种情况,ptmalloc为每个线程设置了一个缓冲区,这个缓冲区也就是tcache
因此tcache指针实际上位于TLS区域内,是各个线程独有的
glibc2.31下的Tcache检查
对于每一个tcache中的chunk,增加了一个key指针,用于指向所属的tcache结构体
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;
当chunk被放入时会设置key指针
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]); //计数增加
}
在free时,会进行检查,最笨的方法就是对于每一个chunk,我都遍历一遍链表,这种方法正确,但是会大大影响效率
ptmalloc使用了一种更机智的方法,在不影响效率的前提下,完成了对double free的检查,详细的看注释
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) //通过检查,放入