目录
Tcache机制的简述
在TCache机制中,它为每个线程创建一个缓存,里面包含一些小堆块,无需对arena上锁即可使用,这种无锁分配算法能有不错的效率提升。
在Glibc的2.26中 新增了Tcache机制 这是ptmalloc2的缓存机制
Tcache在glibc中是默认开启的,在Tcache被开启的时候会定义如下东西
#if USE_TCACHE
/* We want 64 entries. This is an arbitrary limit, which tunables can reduce. */
# define TCACHE_MAX_BINS 64
# define MAX_TCACHE_SIZE tidx2usize (TCACHE_MAX_BINS-1)
/* Only used to pre-fill the tunables. */
# define tidx2usize(idx) (((size_t) idx) * MALLOC_ALIGNMENT + MINSIZE - SIZE_SZ)
/* When "x" is from chunksize(). */
# define csize2tidx(x) (((x) - MINSIZE + MALLOC_ALIGNMENT - 1) / MALLOC_ALIGNMENT)
/* When "x" is a user-provided size. */
# define usize2tidx(x) csize2tidx (request2size (x))
/* With rounding and alignment, the bins are...
idx 0 bytes 0..24 (64-bit) or 0..12 (32-bit)
idx 1 bytes 25..40 or 13..20
idx 2 bytes 41..56 or 21..28
etc. */
/* 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为每个线程都预留了这样一个特殊的bins, bin的数量是64个 每个bin中最多缓存7个chunk。在64位系统上以0x10的字节递增,从24递增到1032字节。32位系统上则从12到512字节,所以Tcache缓存的是非Large Chunk的chunk。
然后引入两个新数据结构来管理Tcache中的bin
/* We overlay this structure on the user-data portion of a chunk when
the chunk is stored in the per-thread cache. */
typedef struct tcache_entry
{
struct tcache_entry *next;
} tcache_entry;
/* There is one of these for each thread, which contains the
per-thread cache (hence "tcache_perthread_struct"). Keeping
overall size low is mildly important. Note that COUNTS and ENTRIES
are redundant (we could have just counted the linked list each
time), this is for performance reasons. */
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_perthread_struct位于堆的开头,也是一个堆块,大小为0x250,为每个线程分配一个的总的bin的管理结构,有两个字段 一个字段counts记录对应Tcache的bin中现有的bin数量, 第二个字段entries用来具体指向相应bin中的chunk块 这个字段类似于fastbin的单链表结构来串连 因为只有一个next指针进行指向
选取一张来自CTF-WIKI的图来描述这种串连情况就是如下所示
可以看到对应chunk的原本的fd域 在Tcache中就是tcache_entry的next域被填充为了指向下一个chunk的索引指针
tcache的初始化操作如下:
static void
tcache_init(void)
{
mstate ar_ptr;
void *victim = 0;
const size_t bytes = sizeof (tcache_perthread_struct); //获得malloc需要的字节数
if (tcache_shutting_down)
return;
arena_get (ar_ptr, bytes);
victim = _int_malloc (ar_ptr, bytes);//使用malloc为该结构分配内存
if (!victim && ar_ptr != NULL)
{
ar_ptr = arena_get_retry (ar_ptr, bytes);
victim = _int_malloc (ar_ptr, bytes);
}
if (ar_ptr != NULL)
__libc_lock_unlock (ar_ptr->mutex);
/* In a low memory situation, we may not be able to allocate memory
- in which case, we just keep trying later. However, we
typically do this very early, so either there is sufficient
memory, or there isn't enough memory to do non-trivial
allocations anyway. */
if (victim)
{
tcache = (tcache_perthread_struct *) victim; //存放
memset (tcache, 0, sizeof (tcache_perthread_struct)); //清零
}
}
使用方法:
首先看能够触发tcache中放入chunk的操作:
释放堆块时,在fastbins的操作前,如果chunk符合大小要求,并且对应bins(即entries对应大小的bins)还未装满,就将其放进去
static void
_int_free (mstate av, mchunkptr p, int have_lock)
{
......
......
#if USE_TCACHE
{
size_t tc_idx = csize2tidx (size);
if (tcache
&& tc_idx < mp_.tcache_bins // 64
&& tcache->counts[tc_idx] < mp_.tcache_count) // 7
{
tcache_put (p, tc_idx);
return;
}
}
#endif
......
......
分配堆块时,出发点有三个:
1.malloc的chunk_size是fast chunk的情形
此时会将victim之后的fast chunk挂入对应的tcache bin链表中 只有tcache->counts[tc_idx]的数量达到7或者fastbin中没有chunk才结束放入需要注意的是 chunks 在 tcache bin 的顺序和在 fastbin 中的顺序是反过来的。
#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 over. */
while (tcache->counts[tc_idx] < mp_.tcache_count
&& (pp = *fb) != NULL)
{
REMOVE_FB (fb, tc_victim, pp);
if (tc_victim != 0)
{
tcache_put (tc_victim, tc_idx);
}
}
}
#endif
2.small bins与fastbins情况类似,双链表中剩余的chunk会填充到tcache中直达上限
#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 over. */
while (tcache->counts[tc_idx] < mp_.tcache_count
&& (tc_victim = last (bin)) != bin)
{
if (tc_victim != 0)
{
bck = tc_victim->bk;
set_inuse_bit_at_offset (tc_victim, nb);
if (av != &main_arena)
set_non_main_arena (tc_victim);
bin->bk = bck;
bck->fd = bin;
tcache_put (tc_victim, tc_idx);
}
}
}
#endif
3.binning code(合并chunk等其他情况中),每一个符合的chunk都会优先放入tcache中,而不是直接返回(除非已满)。然后,程序会从tcache中返回一个chunk
#if USE_TCACHE
/* Fill cache first, return to user only if cache fills.
We may return one of these chunks later. */
if (tcache_nb
&& tcache->counts[tc_idx] < mp_.tcache_count)
{
tcache_put (victim, tc_idx);
return_cached = 1;
continue;
}
else
{
#endif
接下来是从tcache取出chunk的操作
1.在libc_malloc()调用int-malloc之前,如果tcache bin中有符合的chunk,则直接将它返回。(源码就不打了0.0)。
2.bining code中如果放入的chunk达到了上限,则会直接返回一个chunk,bining code结束后,如果没有直接返回,那么如果有一个符合要求的chunk被找到,则返回最后一个。
最后,需要注意的是:tcache中的chunk不会被合并,无论是相邻的chunk,还是chunk和top chunk都不会。这是因为这些chunk的PREV—INUSE位会被标记。
安全性分析
函数tcache_put()的实现
/* Caller must ensure that we know tc_idx is valid and there's room
for more chunks. */
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);
e->next = tcache->entries[tc_idx];
tcache->entries[tc_idx] = e;
++(tcache->counts[tc_idx]);
}
函数tcache_get()的实现
/* Caller must ensure that we know tc_idx is valid and there's
available chunks to remove. */
static __always_inline 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]); // 获得一个 chunk,counts 减一
return (void *) e;
}
这两个函数都假设调用者已经对参数进行了检查,但是由于tcache的操作在free和malloc中往往都处于很靠前的位置,导致很多检查失效,这样做虽然有利于提高效率,但是对安全性造成了负面影响。
另外在tcache-get()函数中:assert (tcache->entries[tc_idx] >0);本意是检查tcache的chunk数量大于零,否则counts可能会发生整数溢出成负数,已经在libc-2.28进行了修复。
libc2.26中tcache机制被发现了漏洞,由于-libc-malloc()使用request2size()来将请求大小转化为实际块大小,该函数不会进行整数溢出检查,所以请求一个非常大的块时,就会导致整数溢出,从而导致错误的返回tcache bin中的块。(CVE-2017-17426),这个问题在libc2.27中已经被修复(使用check-request2size()代替使用request2size()),实现了对整数溢出的检查。
同时在libc2.28中,对tcache加入了二次释放的检查,即在tcache-entry中加入一个标志key,用于表示chunk是否已经在tcache bin中。
利用手法
1.tcache poisoning
修改tcache中的chunk的next指针
例题:HIBT CTF 2018:gundam
提取链接:CTF/2018/Hitbxctf/gundam at master · Ch4nc3n/CTF · GitHub