tcache attack glibc 2.26
tcache 是 glibc 2.26 (ubuntu 17.10) 之后引入的一种技术,它带来了很多新的堆攻击方式。
相关结构体
tcache_entry
typedef struct tcache_entry
{
struct tcache_entry *next;
} tcache_entry;
这个结构体用于储存free后的tcache堆块,单链表结构,next指针指向下一个堆块的usr data部分(fastbin指向的chunk_header部分),同时采用FILO(先进后出)的存取方式。
tcache_perthread_struct teache的管理器
typedef struct tcache_perthread_struct
{
char counts[TCACHE_MAX_BINS]; //tcache链上的空闲堆块个数,每个链上最多7个
tcache_entry *entries[TCACHE_MAX_BINS];
} tcache_perthread_struct;
# define TCACHE_MAX_BINS 64
static __thread tcache_perthread_struct *tcache = NULL;
wiki上的图特别好:
工作方式
- 第一次malloc,会先申请个堆块存放tcache_prethread_struct
- free的堆块先放入tcache(size合适的话)
- tcache对应链表没满之前,先放入该链表,满了之后才放入fastbin或者unsorted bin这些。
- malloc时先从对应tcache中取
- tcache 为空时,如果 fastbin/smallbin/unsorted bin 中有 size 符合的 chunk,会先把 fastbin/smallbin/unsorted bin 中的 chunk 放到 tcache 中,直到填满。之后再从 tcache 中取;因此 chunk 在 bin 中和 tcache 中的顺序会反过来
源码分析
__libc_malloc —> 在源码的最前面添加了tcache相关代码
void *
__libc_malloc (size_t bytes)
{
......
......
#if USE_TCACHE
/* int_free also calls request2size, be careful to not pad twice. */
size_t tbytes;
// 根据 malloc 传入的参数计算 chunk 实际大小,并计算 tcache 对应的下标
checked_request2size (bytes, tbytes); //求出chunk实际大小
size_t tc_idx = csize2tidx (tbytes); //计算对应idx
// 初始化 tcache ,tcache为空(第一次malloc)的时候调用建立tcache_prethread_struct
MAYBE_INIT_TCACHE ();
DIAG_PUSH_NEEDS_COMMENT;
if (tc_idx < mp_.tcache_bins // 根据 size 得到的 idx 在合法的范围内
/*&& tc_idx < TCACHE_MAX_BINS*/ /* to appease gcc */
&& tcache //所需的tcache在链表中存在
&& tcache->entries[tc_idx] != NULL) // tcache->entries[tc_idx] 有 chunk
{
return tcache_get (tc_idx); //从链表中取出所需的chunk
}
DIAG_POP_NEEDS_COMMENT;
#endif
......
......
}
内存释放
tcache_put()
static __always_inline void
tcache_put (mchunkptr chunk, size_t tc_idx)
{
tcache_entry *e = (tcache_entry *) chunk2mem (chunk); //找到对应chunk
assert (tc_idx < TCACHE_MAX_BINS); //idx合法判断
e->next = tcache->entries[tc_idx]; // 这两个操作是把free的chunk放入了链表的头部,对应了FILO的规则
tcache->entries[tc_idx] = e;
++(tcache->counts[tc_idx]); //对应tcache数量加1
}
我们可以看出没什么大的保护操作,没有指针置0。
__libc_free 变化不大 ,变化主要是在_int_free里面
void
__libc_free (void *mem)
{
......
......
MAYBE_INIT_TCACHE (); //tcache不为空时没用
ar_ptr = arena_for_chunk (p);
_int_free (ar_ptr, p, 0);
}
_int_free
static void
_int_free (mstate av, mchunkptr p, int have_lock)
{
......
......
#if USE_TCACHE
{
size_t tc_idx = csize2tidx (size); //求出对应的idx
if (tcache
&& tc_idx < mp_.tcache_bins // 64 idx合法,tcache链表中数量小于等于7
&& tcache->counts[tc_idx] < mp_.tcache_count) // 7
{
tcache_put (p, tc_idx); //放入链表
return;
}
}
#endif
内存申请
从libcmalloc里面我们可以看出是如何进入tcache_get的。
tcache_get()
static __always_inline void *
tcache_get (size_t tc_idx)
{
tcache_entry *e = tcache->entries[tc_idx];
assert (tc_idx < TCACHE_MAX_BINS); //判断idx是否合法
assert (tcache->entries[tc_idx] > 0); //取出来的指针不为0,
tcache->entries[tc_idx] = e->next; //取出后链表的补齐
--(tcache->counts[tc_idx]); // 获得一个 chunk,counts 减一
return (void *) e;
}
我们可以看出其检查很弱,差不多算是tcache->entries[tc_idx] = e->next;这个注意点,意思差不多就是free的tcache里面next指针要存在,不过也可以伪造。
如果没有链表里没有合适的tcache,那么于之前libc的操作类似,不过这样我们看出tcache的优先级,源码中先对其进行判断。
tcache机制带来的内存分配变化
malloc时会把内存块移入tcache
- 首先,申请的内存块符合 fastbin 大小时并且在 fastbin 内找到可用的空闲块时,会把该 fastbin 链上的其他内存块放入 tcache 中。
- 其次,申请的内存块符合 smallbin 大小时并且在 smallbin 内找到可用的空闲块时,会把该 smallbin 链上的其他内存块放入 tcache 中。
- 当在 unsorted bin 链上循环处理时,当找到大小合适的链时,并不直接返回,而是先放到 tcache 中,继续处理。
tcache 所带来的的相关攻击方式
tcache poisoning
伪造next指针就可以任意地址读写,修改之后,两次malloc就可以把相应堆块拿出来。
tcache dup
适用于libc2.27 2.26,但是2.29出现了相关检查,2.28还不太清楚奥
我们从上面的源码可以知道tcache_put()也没有什么检查,算是判断了一下idx是否合法,那么就可以直接double free,而且不需要想fastbin里面那样担心fasttop的检查
tcache perthread corruption
tcache_perthread_struct管理tcache结构,它还是个堆块,那么我们之前说的任意地址读写也可以用来控制这个堆块。
tcache house of spirit
类似于之前glibc的house of spirit,不过更加简单,因为你只需要寻找两个相邻的内存单元,伪造出size和next指针,直接free操作,就可以实现对应大小对应地址堆块的控制。
smallbin unlink
在 smallbin 中包含有空闲块的时候,会同时将同大小的其他空闲块,放入 tcache 中,此时也会出现解链操作,但相比于 unlink 宏,缺少了链完整性校验。因此,原本 unlink 操作在该条件下也可以使用。这个的意思就是之前的unlink不需要绕过链表中对应堆块是否正确的检查了,更加简单,威力也更大了。
libc leak
和之前的unsorted bin attack大部分一样,不过要先把对应的tcache消耗完
Tcache Check 相关影响写到了tcache dup里面
这个其实就是使得tcache dup操作失效了。
index 6d7a6a8..f730d7a 100644 (file)
--- a/malloc/malloc.c
+++ b/malloc/malloc.c
@@ -2967,6 +2967,8 @@ mremap_chunk (mchunkptr p, size_t new_size)
typedef struct tcache_entry
{
struct tcache_entry *next;
+ /* This field exists to detect double frees. */
+ struct tcache_perthread_struct *key;
} tcache_entry;
/* There is one of these for each thread, which contains the
@@ -2990,6 +2992,11 @@ tcache_put (mchunkptr chunk, size_t tc_idx)
{
tcache_entry *e = (tcache_entry *) chunk2mem (chunk);
assert (tc_idx < TCACHE_MAX_BINS);
+
+ /* Mark this chunk as "in the tcache" so the test in _int_free will
+ detect a double free. */
+ e->key = tcache;
+
e->next = tcache->entries[tc_idx];
tcache->entries[tc_idx] = e;
++(tcache->counts[tc_idx]);
@@ -3005,6 +3012,7 @@ tcache_get (size_t tc_idx)
assert (tcache->entries[tc_idx] > 0);
tcache->entries[tc_idx] = e->next;
--(tcache->counts[tc_idx]);
+ e->key = NULL;
return (void *) e;
}
@@ -4218,6 +4226,26 @@ _int_free (mstate av, mchunkptr p, int have_lock)
{
size_t tc_idx = csize2tidx (size);
+ /* Check to see if it's already in the tcache. */
+ tcache_entry *e = (tcache_entry *) chunk2mem (p);
+
+ /* This test succeeds on double free. However, we don't 100%
+ trust it (it also matches random payload data at a 1 in
+ 2^<size_t> chance), so verify it's not an unlikely coincidence
+ before aborting. */
+ if (__glibc_unlikely (e->key == tcache && 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. */
+ }
+
if (tcache
&& tc_idx < mp_.tcache_bins
&& tcache->counts[tc_idx] < mp_.tcache_count)
相关例题可以看看我的2020_heap_practice博文里面的2018LCTF eaay_heap
相关参考:
https://ctf-wiki.github.io/ctf-wiki/pwn/linux/glibc-heap/implementation/tcache-zh/