Tcache

Tcache

Thread local caching

Tcache 是 glibc 2.26 (ubuntu 17.10) 之后引入的,其目的是为了提升堆管理的性能,性能的提升总是伴随更多的安全问题

相关结构体

tcache_entry

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

next指向下一个相同大小的free chunk的data区,而不是其他bin通常指向的chunk头 LIFO

2.29 版本以后的 tcache_entry 增加了key字段防止doublefree等攻击

typedef struct tcache_entry
{
struct tcache_entry *next;
/* This field exists to detect double frees. */
struct tcache_perthread_struct *key;
} tcache_entry;

glibc 2.32以后这里的next字段也会不同,会进行异或加密,后面再说,最新版本的可以值好像也进行了加密

tcache_perthread_struct

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

# define TCACHE_MAX_BINS 64

static __thread tcache_perthread_struct *tcache = NULL;

每个 thread 都会维护一个 tcache_perthread_struct,通常是在第一个堆块,用来管理tcache

counts 记录了 tcache_entry 链上空闲 chunk 的数目,每条链上最多可以有 7 个 chunk

最多TCACHE_MAX_BINS 64个tcache bin从0x20-0x410 按0x10递增

Tcache how 2 work

  1. 第一次malloc,分配一块内存给tcache_perthread_struct,管理tcache
  2. free时,size<0x410(64位带chunke head)时,tcache链表少于7个未满,先放入tcaceh,满后和之前一样fastbin或其他合适bin,inuse位不会置零,不会合并
  3. malloc时在tcache范围内,tcache有就拿,tcache没有时,如果fastbin/smallbin/unsorted bin 中有 size 符合的 chunk,会先把 fastbin/smallbin/unsorted bin 中的 chunk 放到 tcache 中,直到填满。之后再从 tcache 中取;因此 chunk 在 其他bin 中的顺序和 拿到tcache 中的顺序会反过来

RTFSC

申请

  // 从 tcache list 中获取内存
if (tc_idx < mp_.tcache_bins // tc_idx由 size 计算的 idx 在合法范围内
/*&& tc_idx < TCACHE_MAX_BINS*/ /* to appease gcc */
&& tcache
&& tcache->entries[tc_idx] != NULL) // 该条 tcache 链不为空
{
return tcache_get (tc_idx);
}
DIAG_POP_NEEDS_COMMENT;
#endif
// 进入与无 tcache 时类似的流程
if (SINGLE_THREAD_P)
{
victim = _int_malloc (&main_arena, bytes);
assert (!victim || chunk_is_mmapped (mem2chunk (victim)) ||
&main_arena == arena_for_chunk (mem2chunk (victim)));
return victim;
}

tcache->entries[tc_idx]不为空时tcache_get(tc_idx)申请

/* 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];//把最新的tcacehchunk拿出来
assert (tc_idx < TCACHE_MAX_BINS); //判断index合法性
assert (tcache->entries[tc_idx] > 0); //判断tcache bin链是不是为空
tcache->entries[tc_idx] = e->next;//把tcache bin链指向下一个chunk
--(tcache->counts[tc_idx]); // 获得一个 chunk,counts 减一
return (void *) e;
}

几乎没有保护

glibc2.32

static __always_inline void *
tcache_get (size_t tc_idx)
{
tcache_entry *e = tcache->entries[tc_idx];
if (__glibc_unlikely (!aligned_OK (e)))
malloc_printerr ("malloc(): unaligned tcache chunk detected");
tcache->entries[tc_idx] = REVEAL_PTR (e->next);
--(tcache->counts[tc_idx]);
e->key = NULL;
return (void *) e;
}
#define PROTECT_PTR(pos, ptr) \
((__typeof (ptr)) ((((size_t) pos) >> 12) ^ ((size_t) ptr)))
#define REVEAL_PTR(ptr) PROTECT_PTR (&ptr, ptr)

取出时会进行xor解密,2.32的next会通过PROTECT_PTR异或加密,把存储next的地址右移12位后和next异或后把解密地址(也就是chunk真实地址)放到tcache struct中,这让我们之前fastbin attach类似的伪造chunk在这里难道增加

释放

_int_free()

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) // mp_.tcache_count=7
{
tcache_put (p, tc_idx); //p要释放的chunk,tc_idx对应size的entries下标
return;
}
}
#endif
......
......

判断 tc_idx 合法,tcache->counts[tc_idx] 在 7 个以内时,就进入 tcache_put()

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) //chunk要释放的chunk,tc_idx对应size的entries下标
{
tcache_entry *e = (tcache_entry *) chunk2mem (chunk);//得到chunk的tcache_entry结构体
assert (tc_idx < TCACHE_MAX_BINS);//64
e->next = tcache->entries[tc_idx];//chunk的next指向对应size链的最新一个chunk
tcache->entries[tc_idx] = e;//把当前chunk放入 tcache_perthread_struct管理结构
++(tcache->counts[tc_idx]);//++当前size数量
}

依旧几乎没有保护

glibc2.29有了key

_int_free函数里会多会检测 key 字段是否为 tcache,如果相等则检测 free 的指针值是否在对应的tcache_entry 链上,出现则视为程序在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 we get here, it was a coincidence. We've wasted a
few cycles, but don't abort. */
}

glibc2.32

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

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

e->next = PROTECT_PTR (&e->next, tcache->entries[tc_idx]);
tcache->entries[tc_idx] = e;
++(tcache->counts[tc_idx]);
}
#define PROTECT_PTR(pos, ptr) \
((__typeof (ptr)) ((((size_t) pos) >> 12) ^ ((size_t) ptr)))

多了PROTECT_PTR 对当前释放chunk的tcache_entry的next指针变为存储当前next地址和next要指向chunkdata地址异或的值

看个例子

image-20230326234600469

pwndbg帮我们自动解析了

image-20230326234343508

0x55500000c6d9是怎么来的呢? 红色部分异或来的:0x555555559380 xor 0x555555559=0x55500000C6D9