本文目的:在阅读 wiki 中的 Tcache attack 中,发现了不少模糊不清的定义和表述,于是写了这篇博客,若有问题请联系本人邮箱,感谢ing。

1. Tcache overview

在 tcache 中新增了两个结构体,分别是 tcache_entrytcache_perthread_struct

typedef struct tcache_entry
{
struct tcache_entry *next;
} tcache_entry;

typedef struct tcache_perthread_struct
{
char counts[TCACHE_MAX_BINS];
tcache_entry *entries[TCACHE_MAX_BINS];
} tcache_perthread_struct;
static __thread tcache_perthread_struct *tcache = NULL;

tcache_entry:Tcache 链表中的一个空闲内存块(chunk)。
tcache_perthread_struct:每个线程在堆上维护的管理结构,记录了当前线程所有 Tcache 的状态。
其中有两个重要的函数,tcache_get() 和 tcache_put():

static void tcache_put (mchunkptr chunk, size_t tc_idx)
{
tcache_entry *e = (tcache_entry *) chunk2mem (chunk);
assert (tc_idx < TCACHE_MAX_BINS);
e->next = tcache->entries[tc_idx];
tcache->entries[tc_idx] = e;
++(tcache->counts[tc_idx]);
}

static void * 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]);
return (void *) e;
}

这两个函数会在函数 _int_free 和 _libc_malloc 的开头被调用,其中 tcache_put 当所请求的分配大小不大于 0x408 并且当给定大小的 tcache bin 未满时调用。
一个 tcache bin 中的最大块数 mp
.tcache_count 是 7。

/* This is another arbitrary limit, which tunables can change.  Each
tcache bin will hold at most this number of chunks. */
# define TCACHE_FILL_COUNT 7
#endif

在 tcache_get 中,仅仅检查了 tc_idx。此外,我们可以将 tcache 当作一个类似于 fastbin 的单独链表,只是它的 check 并没有 fastbin 那么复杂,仅仅检查 tcache->entries[tc_idx] = e->next;。

2. Tcache Usage

内存释放:

在 free 函数的处理流程初期,程序在完成对齐性与堆块状态的前置检查后,会优先将符合条件的 chunk 回收到 Tcache 中。

_int_free (mstate av, mchunkptr p, int have_lock)
{
// ... (省略部分前置定义)
check_inuse_chunk(av, p);

#if USE_TCACHE
{
size_t tc_idx = csize2tidx (size);

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

内存申请:

在内存分配的 malloc 函数中有多处,会将内存块移入 tcache 中。

a. 针对 Fastbin:

当申请的内存符合 Fastbin 大小且命中可用空闲块时,系统会将该 Fastbin 链上的其余空闲块批量迁移至对应的 Tcache 中。

b. 针对 Smallbin:

当申请的内存属于 Smallbin 范围并在链中找到匹配项时,系统会顺便将该 Smallbin 链上的其他空闲块填入 Tcache。

c. 针对 Unsorted Bin:

在遍历 Unsorted Bin 链时,即使找到大小精确匹配的 chunk,系统也不会立即返回,而是先将其放入 Tcache 并继续遍历。
符合 fastbin 的时候:

if ((unsigned long) (nb) <= (unsigned long) (get_max_fast ()))
{
// ... (查找并取得 victim)
#if USE_TCACHE
/* While we're here, if we see other chunks of the same size,
stash them in the tcache. */
size_t tc_idx = csize2tidx (nb);
if (tcache && tc_idx < mp_.tcache_bins)
{
mchunkptr tc_victim;

/* While bin not empty and tcache not full, copy chunks. */
while (tcache->counts[tc_idx] < mp_.tcache_count
&& (tc_victim = *fb) != NULL)
{
// REMOVE_FB 操作...
tcache_put (tc_victim, tc_idx);
}
}
#endif
void *p = chunk2mem (victim);
alloc_perturb (p, bytes);
return p;
}
}

直接命中 (Direct Hit):

tcache 取出:在内存分配的入口阶段,系统会首先根据申请尺寸判断对应的 Tcache bin 中是否存在可用空闲块。若命中缓存,则直接从中摘取并返回;否则,程序将进入 _int_malloc 执行后续的分配逻辑。

填充后取出 (Stash-and-Get):

Unsorted Bin 阈值控制:在循环处理 Unsorted Bin 链表时,若移入 Tcache 的 chunk 数量达到了设定的最大阈值(由 mp_.tcache_unsorted_limit 控制),分配流程将立即终止并返回。该阈值默认设置为 0,表示对处理数量不设上限。

#if USE_TCACHE
/* If we've processed as many chunks as we're allowed while
filling the cache, return one of the cached ones. */
++tcache_unsorted_count;
if (return_cached
&& mp_.tcache_unsorted_limit > 0
&& tcache_unsorted_count > mp_.tcache_unsorted_limit)
{
return tcache_get (tc_idx);
}
#endif

Unsorted Bin 处理结束返回:在循环处理 unsorted bin 内存块后,如果之前曾放入过 tcache 块,则会取出一个并返回。

#if USE_TCACHE
/* If all the small chunks we found ended up cached, return one now. */
if (return_cached)
{
return tcache_get (tc_idx);
}
#endif

后续实例会在学习完wiki上所有的堆攻击内容后一并补充,