libc版本检查机制

前言

本篇本章是以libc-2.23.so为基础,去对比之后版本的差异问题

libc-2.27.so

Tcache

tcache是在libc-2.27.so引进的一种新机制

tcache_entry

1
2
3
4
5
6
/* We overlay this structure on the user-data portion of a chunk when
the chunk is stored in the per-thread cache. */
typedef struct tcache_entry
{
struct tcache_entry *next;
} tcache_entry;

tcache_perthread_struct

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/* 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;

# define TCACHE_MAX_BINS 64

static __thread tcache_perthread_struct *tcache = NULL;
  • tcache_prethread_struct 是整个 tcache 的管理结构,其中有 64 项 entries。每个 entries 管理了若干个大小相同的 chunk,用单向链表 (tcache_entry) 的方式连接释放的 chunk,这一点上和 fastbin 很像
  • 每个 thread 都会维护一个 tcache_prethread_struct
  • tcache_prethread_struct 中的 counts 记录 entries 中每一条链上 chunk 的数目,每条链上最多可以有 7 个 chunk
  • tcache_entry用于链接 chunk 结构体,其中的next指针指向下一个大小相同的 chunk
    • 这里与 fastbin 不同的是 fastbin 的 fd 指向 chunk 开头的地址,而 tcache 的 next 指向 user data 的地方,即 chunk header 之后

简单来说:就是类似fastbin一样的东西,每条链上最多可以有 7 个 chunk,free堆块的时候优先放入tcache中,满了才放入fastbin,unsorted bin,malloc的时候优先去tcache找(tcache的范围是 [0x20, 0x410],超过这个大小的就会放入unsorted bin)

tcache dup:因为前几个版本的 tcache bin是缺乏校验机制的,即使对tcache bin chunk重复释放,也不会引发任何异常。比fastbin chunk的约束更少,一来不检查size域,二来也不检查是否重复释放

tcache_perthread_struct这个结构体是可以释放的,并且可以将它释放到unsorted bin中去(前提是先修改0x250大小堆块的count为7),然后分配这个unsorted bin chunk,可以控制任意地址分配堆内存。

高版本Tcache

一、在libc-2.29.so及以上的版本往tcache结构体添加了一个key来防止double free,判断条件就是tcache_entry的key指针(被释放堆块的bk指针位置上填入tcache的地址)是否等于tcache bin的地址

1
2
3
4
5
6
typedef struct tcache_entry
{
struct tcache_entry *next;
/* This field exists to detect double frees. */
struct tcache_perthread_struct *key;
} tcache_entry;

绕过:利用UAF或者溢出等等,修改被释放堆块next指针

以及Stash机制:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
 if ((unsigned long)(nb) <= (unsigned long)(get_max_fast())) //size在fastbin范围内
{
idx = fastbin_index(nb);
mfastbinptr *fb = &fastbin(av, idx);
mchunkptr pp;
victim = *fb;

if (victim != NULL) //如果有chunk
{
if (SINGLE_THREAD_P)
*fb = victim->fd; //取出头chunk
else
REMOVE_FB(fb, pp, victim);

if (__glibc_likely(victim != NULL))
{
size_t victim_idx = fastbin_index(chunksize(victim));
if (__builtin_expect(victim_idx != idx, 0)) //对fastbin的size检查
malloc_printerr("malloc(): memory corruption (fast)");
check_remalloced_chunk(av, victim, nb);

#if 1 //if USE_TCACHE,Stash过程:把剩下的放入Tcache中
/* While we're here, if we see other chunks of the same size,
stash them in the tcache. */
size_t tc_idx = csize2tidx(nb);
if (tcache && tc_idx < mp_.tcache_bins) //如果属于tcache管辖范围
{
mchunkptr tc_victim;

/* While bin not empty and tcache not full, copy chunks. */
while (tcache->counts[tc_idx] < mp_.tcache_count && (tc_victim = *fb) != NULL) //只要tcache没空,并且fastbin还有chunk
{
if (SINGLE_THREAD_P) //那么就从fastbin中取出
*fb = tc_victim->fd;
else
{
REMOVE_FB(fb, pp, tc_victim);
if (__glibc_unlikely(tc_victim == NULL))
break;
}
tcache_put(tc_victim, tc_idx);//然后放入tcache中
}
}
#endif
void *p = chunk2mem(victim);
alloc_perturb(p, bytes);
return p;
}
}
}

当一个线程申请0x50大小的chunk时,如果tcache没有,那么就会进入分配区进行处理,如果对应bin中存在0x50的chunk,除了取出并返回之外,ptmalloc会认为这个线程在将来还需要相同的大小的chunk,因此就会把对应bin中0x50的chunk尽可能的放入tcache的链表中去

这么做会存在一些问题,对于比较典型的 fastbin double free 产生了一个很有趣的影响:

首先需要先释放7个chunk,填满tcache,然后Free(C7) Free(C8) Free(C7),在fastbin中构造出环

下一步,为了分配到fastbin,需要先申请7个,让Tcache为空,再次申请时就会使用fastbin中的C7,这一步是整个手法的精华。取出C7后,Stash会把fastbin链表中的chunk全部放入Tcache中,而C7又是被我们分配到的堆块,是可控的,这就导致我们不需要伪造size字段,获得了一个真正的任意写。

二、在 libc-2.32.so版本中新加入了一个 key 会对 tcache next 的内容进行异或

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
static __always_inline void 
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;
e->next = PROTECT_PTR (&e->next, tcache->entries[tc_idx]); //不是直接赋值next
tcache->entries[tc_idx] = e;
++(tcache->counts[tc_idx]);
}
static __always_inline void *
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 = NULL;
return (void *) e;
}
1
2
3
4
5
6
7
8
9
10
11
12
/* Safe-Linking:
Use randomness from ASLR (mmap_base) to protect single-linked lists
of Fast-Bins and TCache. That is, mask the "next" pointers of the
lists' chunks, and also perform allocation alignment checks on them.
This mechanism reduces the risk of pointer hijacking, as was done with
Safe-Unlinking in the double-linked lists of Small-Bins.
It assumes a minimum page size of 4096 bytes (12 bits). Systems with
larger pages provide less entropy, although the pointer mangling
still works. */
#define PROTECT_PTR(pos, ptr) \
((__typeof (ptr)) ((((size_t) pos) >> 12) ^ ((size_t) ptr)))
#define REVEAL_PTR(ptr) PROTECT_PTR (&ptr, ptr)

PROTECT_PTR:对 pos 右移了 12 位(去除了末尾的 3 位十六进制信息),再异或ptr。

即:被释放堆块fd=(被释放堆块fd所在地址>>12)^被释放堆块的前一个堆块fd所在地址。

而这里的 key 就是储存内容的指针(在代码中叫做 pos),在放入的时候让内容与这个 key 进行异或再储存,在取出的时候让内容与这个 key 进行异或再取出。而得益于这个秘钥就是储存内容的指针,所以无需使用其他空间来放置这个 key 的内容,只需要保存异或之后的内容,在解密时只需 PROTECT_PTR (&ptr, ptr) 这样操作即可。

需要注意的是,当 tcache 中只有一个元素的时候,也就是在放入这个元素的过程中,tcache->entries[tc_idx] == 0,在这个时候放入元素的时候会异或 0,也就是在 e->next 位置存放的内容正好就是 key 的信息,因为 key 异或 0 还是秘钥。而且就算之后加入了其他的元素,这个元素始终还是在链表的尾部,所以内容不会发生变化

绕过:

  • 通过 0 异或秘钥还是秘钥的这个特性,当 tcache 链上只有一个指针的时候,我们就可以通过 show 函数来 leak 出秘钥的信息,有了秘钥的信息之后,我们就可以伪造秘钥信息了
  • 可以通过 largebin 来泄露堆地址,由于 key 是当前指针 >> 12,所以我们可以确保在 4096 字节内这个 key 都是正确的

利用

一、泄露堆基址

构造两个相同 size 的堆块 a 和 b,我们先 free (a) 让他进入到 tcache 中,再 free (b) 也让他进入到 tcache 中。这时候,在堆块 b 的 fd 位置就存在着堆块 a 的地址,我们 leak 出来就能够得到堆地址

libc-2.29.so

1
2
3
4
5
6
7
8
9
10
11
if (__glibc_unlikely (size <= 2 * SIZE_SZ) || __glibc_unlikely (size > av->system_mem)) 				malloc_printerr ("malloc: invalid size (unsorted)"); 
if (__glibc_unlikely (chunksize_nomask (next) < 2 * SIZE_SZ) || __glibc_unlikely (chunksize_nomask (next) > av->system_mem))
malloc_printerr ("malloc: invalid next size (unsorted)");
if (__glibc_unlikely ((prev_size (next) & ~(SIZE_BITS)) != size))
malloc_printerr ("malloc: mismatching next->prev_size (unsorted)");
if (__glibc_unlikely (bck->fd != victim) || __glibc_unlikely (victim->fd != unsorted_chunks (av))) malloc_printerr ("malloc: unsorted double linked list corrupted");
if (__glibc_unlikely (prev_inuse (next)))
malloc_printerr ("malloc: invalid next->prev_inuse (unsorted)"); ...... ...... /* remove from unsorted list */ if (__glibc_unlikely (bck->fd != victim))
malloc_printerr ("malloc: corrupted unsorted chunks 3");
unsorted_chunks (av)->bk = bck;
bck->fd = unsorted_chunks (av);
  • 对于unsortbin的解链添加了验证链完整性的检查,让 unsortbin attack失效

    • 绕过:

      1.largebin中的chunk->fd_nextsize=0;

      2.largebin中的chunk->bk_nextsize可控制;

      3.unsortedbin里的chunk大于largebin,并且如果进入largebin,是同一个index。

类unlink手法,高版本一样可用,至少目前我知道的:到 libc-2.32 是没问题的

利用条件

1.smallbin中可以控制大小为size块的bk指针

2.tcache中大小为size块的个数为6

3.申请堆块是calloc

过程

  • 释放6个0x100的chunk到tcache bin中
  • 构造两个0x100的small bin(利用Unsorted bin或Large bin切割得到)
  • 修改后插入的small bin的 bk 指针为目标地址-0x10,且保持fd指针不变
  • 用calloc分配0x100的chunk

结果

  • 在目标地址上写入原本small bin上的 bk 指针内容

libc-2.32.so

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
if (SINGLE_THREAD_P)  
{
/* Check that the top of the bin is not the record we are going to
add (i.e., double free). */
if (__builtin_expect(old == p, 0))
malloc_printerr("double free or corruption (fasttop)");
p->fd = PROTECT_PTR(&p->fd, old);
*fb = p;
}
else
do
{
/* Check that the top of the bin is not the record we are going to
add (i.e., double free). */
if (__builtin_expect(old == p, 0))
malloc_printerr("double free or corruption (fasttop)");
old2 = old;
p->fd = PROTECT_PTR(&p->fd, old);
} while ((old = catomic_compare_and_exchange_val_rel(fb, p, old2))
!= old2);

上面是glibc2.32下的fastbin源码,同样存在跟tcache一样的保护机制,会对 fd 指针进行异或处理

1
2
if (__glibc_unlikely (!aligned_OK (e)))
malloc_printerr ("malloc(): unaligned tcache chunk detected");

由于这个检测的存在,我们 tcache 申请的地址似乎要做到 0x10 对齐(x64)

libc-2.34.so

该版本删除了各种 hook 函数,所以要更换思路,一般可以通过 FSOP 攻击输出流的函数虚表

查看评论