我们先分析一下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");
至此原因及原理,讲述完毕