高版本下tcache bin如何通过key值绕过double free
我们先分析一下tcache chunk这个结构体(chunk进入了tcache bin时的结构体) 下面是libc 2.35源码中对进入到tcache bin的chunk使用的结构体 typedef struct tcache_entry { struct tcache_entry *next; /* This field exists to detect double frees. *
我们先分析一下tcache chunk这个结构体(chunk进入了tcache bin时的结构体)
下面是libc 2.35源码中对进入到tcache bin的chunk使用的结构体
typedef struct tcache_entry
{
struct tcache_entry *next;
/* This field exists to detect double frees. */
uintptr_t key;
} tcache_entry;
源码分析
对于tcache chunk的next值,这里我们也浅分析一下,这里截取了_int_free对tcache的源码部分:
#if USE_TCACHE
{
size_t tc_idx = csize2tidx (size);
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);
/* 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_key))
{
tcache_entry *tmp;
size_t cnt = 0;
LIBC_PROBE (memory_tcache_double_free, 2, e, tc_idx);
for (tmp = tcache->entries[tc_idx];
tmp;
tmp = REVEAL_PTR (tmp->next), ++cnt)
{
if (cnt >= mp_.tcache_count)
malloc_printerr ("free(): too many chunks detected in tcache");
if (__glibc_unlikely (!aligned_OK (tmp)))
malloc_printerr ("free(): unaligned chunk detected in tcache 2");
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->counts[tc_idx] < mp_.tcache_count)
{
tcache_put (p, tc_idx);
return;
}
}
}
这里我们先关注一下,chunk入tcache bin的部分,我们tcache chunk的next值是如何来的
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_key;
e->next = PROTECT_PTR (&e->next, tcache->entries[tc_idx]);
tcache->entries[tc_idx] = e;
++(tcache->counts[tc_idx]);
}
tcahce_put先把得到chunk指针转化为定义的tcache_entry,这里chunk2mem是返回我们chunk的mem区域,mem:
chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Size of previous chunk, if unallocated (P clear) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Size of chunk, in bytes |A|M|P|
mem-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| User data starts here... .
. .
. (malloc_usable_size() bytes) .
. |
nextchunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| (size of chunk, but used for application data) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Size of next chunk, in bytes |A|0|1|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
然后就是把tcache_key传入到e->key的位置,next值是通过tcache->entries[tc_idx]的值进行计算的,tc_idx是我们上面对应对应大小tcache bin的相关下标,也就是取出对应tcache bin最后一个chunk( tcache bin 是先进后出)
#define PROTECT_PTR(pos, ptr) \
((__typeof (ptr)) ((((size_t) pos) >> 12) ^ ((size_t) ptr)))
泄露heap地址原理 && 任意申请原理
通过PROTECT_PTR宏定义,我们知道next的值,是我们free的chunk的指针与上一个在对应tcache bin的chunk的指针进行加密操作,所以,我们在泄露heap地址时,释放一个tcache chunk大小范围的chunk时,如果它是对应tcache bin的第一个chunk,那么它的next值是这样的
(chunk_mem_addr >> 12) & tcache_chunk_last
(chunk_mem_addr >> 12) & 0 // tcache_chunk_last 为 空
所以我们泄露它并计算出来heap地址,又因为地址是按页加载的,也就是0x1000为开始,是12位bit大小,所以后面12位bit对于我们不是很重要,即是它被右移了,我们也可以左移回去
这也就是我们通过tcache chunk泄露地址的原理
高版本添加了这一个异或next指针的特性,只是为了检测取出chunk的合法性,我们从_libc_malloc截取源码:
#if USE_TCACHE
/* int_free also calls request2size, be careful to not pad twice. */
size_t tbytes;
if (!checked_request2size (bytes, &tbytes))
{
__set_errno (ENOMEM);
return NULL;
}
size_t tc_idx = csize2tidx (tbytes);
MAYBE_INIT_TCACHE ();
DIAG_PUSH_NEEDS_COMMENT;
if (tc_idx < mp_.tcache_bins
&& tcache
&& tcache->counts[tc_idx] > 0)
{
victim = tcache_get (tc_idx);
return tag_new_usable (victim);
}
DIAG_POP_NEEDS_COMMENT;
抛开对大小和对应tcache bin的count检测等等,我们可以定位到这个函数
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 = 0;
return (void *) e;
}
这里又添加了一个取出检测的机制aligned_OK也就是检查对应的chunk指针是不是以0位结尾的,防止我们任意伪造位置,这个在fastbin取chunk过程也有对应的检测,这个对于tcache bin来说就没有特别恶心,源码如下:
#define aligned_OK(m) (((unsigned long)(m) & MALLOC_ALIGN_MASK) == 0)
这里最重要的是这个宏定义REVEAL_PTR:
#define REVEAL_PTR(ptr) PROTECT_PTR (&ptr, ptr)
看得出来就是对于我们上面进行一个解密操作,如果我们想控制tcache bin链,至少是需要泄露出来heap地址,计算出来当前对应tcahce bin的chunk指针的地址,然后对我们想要申请的地址进行加密操作也就是
(tcache_bin_chunk_last >> 12 ) & (target_chunk_addr)
这样就可以控制对应tcache bin最后的chunk的next指针,使得我们可以控制tcache bin的链表,实现任意申请地址
如何绕过tcache的double free检测
结束掉前面的abalabala,我们来到这次的重点
在很多条件下,我们有很多种方式去绕过它,例如:
最轻松的莫过于可以记录多个chunk指针且我们free完后可以修改,可以进行A->B,然后对A的next值进行修改就可以A->C
当free完后不能进行修改,我们可以tcache bin填满进入fastbin,再通过A->b->A进行绕过,然后可以同上操作
但是如果我们只能记录一个chunk指针,那么我们便无法满足count在取出对应的chunk后,还满足不为零的条件,因为我们需要正面绕过tcache的double free的检测
其他绕过方法都比较常见,故这里只讲最后一种
源码分析 二
这里是chunk入tcache bin表链的检测
if (__glibc_unlikely (e->key == tcache_key))
{
tcache_entry *tmp;
size_t cnt = 0;
LIBC_PROBE (memory_tcache_double_free, 2, e, tc_idx);
for (tmp = tcache->entries[tc_idx];
tmp;
tmp = REVEAL_PTR (tmp->next), ++cnt)
{
if (cnt >= mp_.tcache_count)
malloc_printerr ("free(): too many chunks detected in tcache");
if (__glibc_unlikely (!aligned_OK (tmp)))
malloc_printerr ("free(): unaligned chunk detected in tcache 2");
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. */
}
}
看到这个free(): double free detected in tcache 2是不是很眼熟

这个就是我们double free了同一个tcache chunk后的原因,但是绕过方法异常简单e->key == tcache_key,回到我们最上面的结构体
typedef struct tcache_entry
{
struct tcache_entry *next;
/* This field exists to detect double frees. */
uintptr_t key;
} tcache_entry;
在这里就可以知道key是起什么作用的,就是我们释放chunk后,如何判断我们chunk是否已经被释放过,所以我们可以通过可以修改函数把chunk的key给抹去后,再次释放,就可以进入tcache bin的表链
虽然但是,总觉得这个key值过于随便了,emmmm身为一个pwn手,还是不bb赖赖了,到时候更新版本,直接上难度
至于为什么会报这个错,是因为
for (tmp = tcache->entries[tc_idx];
tmp;
tmp = REVEAL_PTR (tmp->next), ++cnt)
把每一个tcache bin里的chunk给遍历出来,刚好tmp值等于我们free掉的chunk指针,满足了这个:
if (tmp == e)
malloc_printerr ("free(): double free detected in tcache 2");
至此原因及原理,讲述完毕