#本文根据wiki和hollk师傅的文章进行学习#
#hollk师傅的文章:https://hollk.blog.youkuaiyun.com/article/details/113400567?spm=1001.2014.3001.5502
概览:
tcache是glibc 2.26(Ubuntu 16之后)之后引入的一种新技术,目的是为了提升堆管理的效率和性能,在tcache中,虽然提升了整体的运行性能,但是改变了很多安全检查,所以就有更多可以利用的漏洞
两个结构体:
tcache中引入了两个新的结构体:tcache_entry tcache_perthread_struct
tcache_entry:
它的数据结构如下:
typedef struct tcache_entry
{
struct tcache_entry *next;
} tcache_entry;
可以看见这个结构体里面只有一个成员:也就是指向下一个tcache_entry的指针(这个chunk要大小相同),那么从宏观上看,tcache_entry的数据结构就是一个单向链表(与fastbins类似)
值得注意的是,在tcache_entry中,指针指向的是结构体数据开始的位置,而不是他的头指针
tcache_perthread_struct:
这个结构体的数据结构如下:
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;
可以看到里面有两个成员:一个counts数组,一个装入 tcache_entry指针的数组,其中的TCACHE_MAX_BINS是宏定义的tcache的最大容纳数
那么我们可以合理的推测,这个tcache_perthread_struct结构体是用来管理tcache_entry的。事实上确实也是,他们之间的关系如下:
(图用的hollk师傅的图,我的画图技术太差了画出来不大美观)
两个重要函数:
tcache_put()
:
static 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_put函数中进行的是将chunk放入tcache_entry结构体的操作,并且是直接将chunk插入到链表头部,没有进行任何检查
tcache_get():
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]);
return (void *) e;
}
这个函数的功能就是上面那个函数的镜像了,这个函数是负责将chunk从tcache中取出来的。这里可以发现在该函数中对tcache中取chunk的操作只进行了一个:只检查了tc_idx 是否大于0(也就是对应这个链表里面还有没有成员)
Tcache Usage:
这里先简述一下tcache的工作流程(根据hollk师傅的总结):
- 在程序第一malloc之时会自动创建并初始化一块内存来存放 tcache_perthread_struct结构体
- 释放chunk时,如果chunk的size小于small bin size,在
进入tcache之前
会先放进fastbin或者unsorted bin中 - 放入tcache后:
- 放到对应大小的tcache之中,知道对应结构下的成员数量达到最大(7个)
- 对应位置的tcache被填满之后,再释放的对应大小的chunk在被释放后就会按照规则放入对应的bin当中
- tcache中的chunk不会互相合并,并且保留了inuse位
- 在程序需要分配chunk时,且需要的chunk的size在tcache的范围内,程序就会在tcache中选取对应chunk,直到tcache为空
- 当tcache为空时,会从bins中找对应大小的chunk
- 当tcache为空时如果fastbin、small bin、unsorted bin中有size符合的chunk,会先把fastbin、small bin、unsorted bin中的chunk放到tcache中,直到填满,之后再从tcache中取
取出chunk:
// 从 tcache list 中获取内存
if (tc_idx < mp_.tcache_bins && tcache && tcache->entries[tc_idx] != NULL)
{
return tcache_get (tc_idx);
}
DIAG_POP_NEEDS_COMMENT;
#endif
}
验证tc_idx是否合法和tcache中是否还有成员后使用tcache_get函数将其取出
内存释放:
_int_free (mstate av, mchunkptr p, int have_lock)
{
INTERNAL_SIZE_T size; /* its size */
mfastbinptr *fb; /* associated fastbin */
mchunkptr nextchunk; /* next contiguous chunk */
INTERNAL_SIZE_T nextsize; /* its size */
int nextinuse; /* true if nextchunk is used */
INTERNAL_SIZE_T prevsize; /* size of previous contiguous chunk */
mchunkptr bck; /* misc temp for linking */
mchunkptr fwd; /* misc temp for linking */
size = chunksize (p);
if (__builtin_expect ((uintptr_t) p > (uintptr_t) -size, 0)
|| __builtin_expect (misaligned_chunk (p), 0))
malloc_printerr ("free(): invalid pointer");
if (__glibc_unlikely (size < MINSIZE || !aligned_OK (size)))
malloc_printerr ("free(): invalid size");
check_inuse_chunk(av, p);
#if USE_TCACHE
{
size_t tc_idx = csize2tidx (size);
if (tcache
&& tc_idx < mp_.tcache_bins
&& tcache->counts[tc_idx] < mp_.tcache_count)
{
tcache_put (p, tc_idx);
return;
}
}
#endif
......
}
先检查了chunk的对齐和前后堆块的释放情况后再检查tc_idx和tcache中的成员数量是否充满后将被释放的chunk放入tcache中
Tcache的利用方式:
tcache poisoning:
通过覆盖 tcache 中的 next,不需要伪造任何 chunk 结构即可实现 malloc 到任何地址(因为tcache并没有对next进行任何检查)。
以wiki上的程序为例子看一下tcache poisoning的流程(做了一些简化):
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
int main()
{
size_t stack_var;
fprintf(stderr, "The address of the malloc() to return is %p.\n", (char*)&stack_var);
intptr_t *a = malloc(128);
intptr_t *b = malloc(128);
free(a);
free(b);
b[0] = (intptr_t)&stack_var;
malloc(128);
intptr_t *c = malloc(128);
fprintf(stderr, "3st malloc(128): %p\n", c);
fprintf(stderr, "We got the control\n");
return 0;
}
上面这个程序就模拟了一次tcache_poisoning的流程:首先定义一个size_t类型的变量stack_var,我们创建两个128字节大小的堆块,再将其释放,修改b[0] 的值为stack_var的地址,之后我们再连续申请两个也是128字节的堆块,并将后面这个申请的堆块赋给指针c
进入gdb看一下这个流程:
首先将断点下在第14行,然后看一下tcache中的情况:
可以看见前两个申请并被释放的128字节的堆块已经进入了tcache中0x90大小的链表中了
然后我们运行到下一行,也就是改变这个free chunk的next指向,再在gdb中看一下:
可以看见这里有一个0x7fffffffdfa8的地址被挂进了tcache中,之后我们将程序运行到第18行,再看一下heap的状态:
这里可以发现我们已经将0x7fffffffdfa8这个位置上的内存作为一个chunk分配出去并可以完全控制它了