Pwn Heap With Tcache
Pwn Heap With Tcache
前言
glibc 2.26
开始引入了 tcache
, 相关的 commit
可以看 这里 。加入 tcache
对性能有比较大的提升,不过由于 tcache
的存在 ,一些利用方式的限制条件就少了许多。具体往下看。
相关文件位于
https://gitee.com/hac425/blog_data/tree/master/tcache_pwn
修改自:https://github.com/andigena/ptmalloc-fanzine/tree/master/05-tcache
源码分析
首先分析分析源码,看看 tcache
的工作原理
相关数据结构
typedef struct tcache_entry
{
struct tcache_entry *next;
} tcache_entry;
typedef struct tcache_perthread_struct
{
char counts[TCACHE_MAX_BINS];
tcache_entry *entries[TCACHE_MAX_BINS]; // TCACHE_MAX_BINS = 64
} tcache_perthread_struct;
tcache
也是使用 类似 bins
方式来管理 tcache
。
tcache_perthread_struct
是整个 tcache
的管理结构, 它的 entries
有 64
项
每一项由 相同大小的 chunk
通过 tcache_entry
使用单向链表链接(类似于fastbin
的链接方式)。
counts
用于记录 entries
中每一项当前链入的 chunk
数目, 最多可以有 7
个 chunk
。
tcache_entry
用于链接 chunk
的结构体, 其中就一个 next
指针,指向下一个相同大小的 chunk
基本操作
下面通过分析对 tcache
的两个基本操作理解上面结构体的作用
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);
e->next = tcache->entries[tc_idx];
tcache->entries[tc_idx] = e; // 增加到链表头部
++(tcache->counts[tc_idx]); // 记录当前 bin 的 chunk数
}
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;
}
tcache_put
用于把一个 chunk
放到 指定的 tcache->entries
里面去, tc_idx
通过 csize2tidx (nb)
计算得到 (nb
是 chunk
的大小)。
它首先把 chunk+2*SIZE_SZ
(就是除去 header
部分) 强制转换成 tcache_entry *
类型,然后插入到 tcache->entries[tc_idx]
的首部,最后把 tcache->counts[tc_idx]
加 1
,表示新增了一个 chunk
到 该 表项。
tcache_get
根据 tc_idx
取出 tcache->entries[tc_idx]
的第一个chunk
, 然后把 指针强制转换为 (void *)
这样就可以大概得到一个图
tcache->entries
的每一项通过 单向链表链接chunk
。tcache_entry
和malloc chunk
是重叠的,tcache_entry->next
和chunk->fd
是一个位置。
tcache in malloc
__libc_malloc
malloc
的入口点是 __libc_malloc
(做了一些注释)
__libc_malloc (size_t bytes)
{
.............
.............
.............
#if USE_TCACHE
/* int_free also calls request2size, be careful to not pad twice. */
size_t tbytes;
checked_request2size (bytes, tbytes); // tbytes 为 bytes请求的 转换后得到的 chunk 的 size
size_t tc_idx = csize2tidx (tbytes); // 根据大小 tbytes , 找到 tcache->entries 索引
MAYBE_INIT_TCACHE ();
DIAG_PUSH_NEEDS_COMMENT;
if (tc_idx < mp_.tcache_bins
/*&& tc_idx < TCACHE_MAX_BINS*/ /* to appease gcc */
&& tcache
&& tcache->entries[tc_idx] != NULL) // 如果 tcache->entries[tc_idx] 有 chunk ,就返回
{
return tcache_get (tc_idx); // 调用 tcache_get 拿到 chunk 然后返回
}
DIAG_POP_NEEDS_COMMENT;
#endif
if (SINGLE_THREAD_P)
{
victim = _int_malloc (&main_arena, bytes);
assert (!victim || chunk_is_mmapped (mem2chunk (victim)) ||
&main_arena == arena_for_chunk (mem2chunk (victim)));
return victim;
}
arena_get (ar_ptr, bytes);
victim = _int_malloc (ar_ptr, bytes);
首先判断 tcache->entries[tc_idx]
里面有没有 chunk
,如果有就直接返回,否则进入 _int_malloc
分配内存。
下面看看 _int_malloc
(主要看 tcache
处理的部分)
_int_malloc
处理fastbin
首先是把 请求的 size
转换成 实际 malloc
内部的 size
,然后定义了一个宏
// 从 fastbin里面移除 pp
#define REMOVE_FB(fb, victim, pp) \
do \
{ \
victim = pp; \
if (victim == NULL) \
break; \
} \
while ((pp = catomic_compare_and_exchange_val_acq (fb, victim->fd, victim)) \
!= victim); \
用于多线程的中从 fastbin
里面移除一个 chunk
.
然后进入分配的流程, 首先如果 size
在 fastbin
的范围内进入, fastbin
分配的流程
if ((unsigned long) (nb) <= (unsigned long) (get_max_fast ()))
{
idx = fastbin_index (nb);
mfastbinptr *fb = &fastbin (av, idx);
mchunkptr pp;
victim = *fb;
if (victim != NULL)
{
if (SINGLE_THREAD_P)
*fb = victim->fd;
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))
malloc_printerr ("malloc(): memory corruption (fast)");
check_remalloced_chunk (av, victim, nb);
#if USE_TCACHE
size_t tc_idx = csize2tidx (nb);
if (tcache && tc_idx < mp_.tcache_bins) // 把该 fastbin 里面其他的 bin 放到 tcache 里面
{
mchunkptr tc_victim;
/* While bin not empty and tcache not full, copy chunks. */
while (tcache->counts[tc_idx] < mp_.tcache_count // 判断 tcache 中指定 bin 中 chunk 是否超过 7
&& (tc_victim = *fb) != NULL)
{
if (SINGLE_THREAD_P)
*fb = tc_victim->fd;
else
{
REMOVE_FB (fb, pp, tc_victim);
if (__glibc_unlikely (tc_victim == NULL))
break;
}
tcache_put (tc_victim, tc_idx);
}
}
#endif
void *p = chunk2mem (victim);
alloc_perturb (p, bytes);
return p;
}
}
}
- 在 相应
fastbin
找到 合适的chunk
后,就把 该chunk
从fastbin
里面拿下来 - 然后 把相应
fastbin
里面剩下的chunk
全都放到tcache
里面 , 直到tcache->entries[tc_idx]
满了 (已经有7
个chunk
了,即tcache->counts[tc_idx] = mp_.tcache_count = 7
)。 - 最后在返回一开始拿到的
chunk
给用户
如果 fastbin
不能分配,则进入 smallbin
的分配流程
处理 smallbin
if (in_smallbin_range (nb))
{
idx = smallbin_index (nb);
bin = bin_at (av, idx);
if ((victim = last (bin)) != bin)
{
bck = victim->bk;
if (__glibc_unlikely (bck->fd != victim))
malloc_printerr ("malloc(): smallbin double linked list corrupted");
set_inuse_bit_at_offset (victim, nb);
bin->bk = bck;
bck->fd = bin; // 找到 chunk , 从 smallbin拿下来准备返回给用户
if (av != &main_arena)
set_non_main_arena (victim);
check_malloced_chunk (av, victim, nb);
#if USE_TCACHE
/* While we're here, if we see other chunks of the same size, // 把指定 smallbin 里面的 bin扔到 tcache里面
stash them in the tcache. */
size_t tc_idx = csize2tidx (nb);
if (tcache && tc_idx < mp_.tcache_bins)
{
mchunkptr tc_victim;
/* While bin not empty and tcache not full, copy chunks over. */
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);
}
}
}
#endif
void *p = chunk2mem (victim);
alloc_perturb (p, bytes);
return p;
}
}
和 fastbin
是类似的操作, 在 size
对应的 smallbin
里面找到 chunk
后
把这个 chunk
从链表上取下来
然后把该 smallbin
里面剩下的 bin
放入到 tcache
, 直到 tcache->entries[tc_idx]
满.
如果 smallbin
也没能分配,进入 unsorted bin
遍历unsorted bin
int iters = 0;
while ((victim = unsorted_chunks (av)->bk) != unsorted_chunks (av))
{
....................
....................
....................
/* remove from unsorted list */
unsorted_chunks (av)->bk = bck;
bck->fd = unsorted_chunks (av);
// 把 bin 从 unsorted bin 里面拿下来后,先放入 tcache
#if USE_TCACHE
// 如果unsorted bin 的大小正好,扔到 tcache ,然后继续遍历
We may return one of these chunks later. */
if (tcache_nb
&& tcache->counts[tc_idx] < mp_.tcache_count)
{
tcache_put (victim, tc_idx);
return_cached = 1;
continue;
}
else
{
#endif
check_malloced_chunk (av, victim, nb);
void *p = chunk2mem (victim);
alloc_perturb (p, bytes);
return p;
#if USE_TCACHE
}
#endif
}
//大小不刚好等于需要的size 的话,就把 bin放到 相应的 bin 里面。
.......................................
.......................................
.......................................
#if USE_TCACHE
//如果有 大小适配的 unsorted bin 进入了 tcache(return_cached=1) 同时 mp_.tcache_unsorted_limit > 0 默认为 0 ,不会进入分支, 继续遍历
++tcache_unsorted_count;
if (return_cached
&& mp_.tcache_unsorted_limit > 0
&& tcache_unsorted_count > mp_.tcache_unsorted_limit)
{
return tcache_get (tc_idx);
}
#endif
.......................................
.......................................
.......................................
} // end of while ((victim = unsorted_chunks (av)->b
//遍历完 unsorted bin 后 ,根据 return_cached 判断 tcache 里面是否有合适的 chunk
#if USE_TCACHE
/* If all the small chunks we found ended up cached, return one now. */
if (return_cached)
{
return tcache_get (tc_idx);
}
#endif
-
在遍历
unsorted bin
的时候, 如果找到大小刚好满足的bin
, 不会立刻返回,而是把这个bin
放入tcache
里面,并且设置return_cached=1
,表示 有 大小适配的unsorted bin
进入了tcache
-
如果大小不是正好满足需要,就走一般的流程,把
bin
放到相应的smallbin
或者largebin
里面 -
遍历
unsorted bin
的最后,会根据return_cached
判断是否有 大小适配的unsorted bin
进入了tcache
,mp_.tcache_unsorted_limit
默认为0
,所以不会进入分支, 这样就会把所有的unsorted bin
都放入到tcache
。 -
遍历完
unsorted bin
后 ,根据return_cached
判断tcache
里面是否有合适的chunk
,有的话就可以返回了 -
否则
large bin
,top chunk
来分配。
tcache in free
static void
_int_free (mstate av, mchunkptr p, int have_lock)
{
size = chunksize (p);
check_inuse_chunk(av, p);
#if USE_TCACHE
{
size_t tc_idx = csize2tidx (size); // tcache bin 的索引
if (tcache
&& tc_idx < mp_.tcache_bins // 64 ,最多 64 个 bin
&& tcache->counts[tc_idx] < mp_.tcache_count) // 7 ,tcache->counts 存放每个 bin 已经存放的 chunk数量
{
tcache_put (p, tc_idx);
return;
}
}
#endif
删掉了一些没影响的代码
- 首先就是获取要释放的
chunk
的size
, 然后判断size
是否符和规范(是否对齐之类的check
), 如果合规就看tcache->counts[tc_idx]
是否已经满了 ,如果没有满就直接放入tcache
, 然后返回。 - 否则就和没有
tcache
是一样的处理
总结
在 free
的时候,会检测 p
的下一个 chunk( next )
的 PREV_INUSE
位,但是如果 chunk
被放入了 tcache ,next->PREV_INUSE
位不会被修改 ,所以还是会标志为 in_used
. 所以我们可以 多次释放同一个 chunk
.
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); // 通过下一个 chunk 的 pre_inused 位,判断当前 chunk 释放已经被释放
#if USE_TCACHE
{
size_t tc_idx = csize2tidx (size); // tcache bin 的索引
if (tcache
&& tc_idx < mp_.tcache_bins // 64 ,最多 64 个 bin
&& tcache->counts[tc_idx] < mp_.tcache_count) // 7 ,tcache->counts 存放每个 bin 已经存放的 chunk数量
{
tcache_put (p, tc_idx); // 如果 chunk 被放入了 tcache ,next->pre_inuse 不会被修改。
return;
}
}
#endif
同时在 malloc
的时候 ,先尝试 tcache
分配
void *
__libc_malloc (size_t bytes)
{
#if USE_TCACHE
size_t tbytes;
checked_request2size (bytes, tbytes);
size_t tc_idx = csize2tidx (tbytes);
if (tc_idx < mp_.tcache_bins
&& tcache
&& tcache->entries[tc_idx] != NULL)
{
return tcache_get (tc_idx);
}
#endif
这也使得很多安全检测不会被执行。
测试
搭建环境
编译glibc
首先编译一个开启 tcache
的 glibc
,我用的是 glibc-2.27
tar xvf glibc-2.27.tar.bz2
mkdir gcc227_build
cd gcc227_build/
../glibc-2.27/configure --prefix=/usr/local/glibc227
sudo mkdir /usr/local/glibc227
sudo make install
可以参考
http://www.sysnote.org/2015/08/25/use-new-glibc/
下载测试代码
下载
https://github.com/andigena/ptmalloc-fanzine/tree/master/05-tcache
作为测试代码。
修改 Makefile
PROGRAMS = tcache_poisoning overlapping_chunks_by_caching tcache_house_of_spirit tcache_dup
CFLAGS += -Wpedantic -std=gnu11 -g -Wl,--rpath=/usr/local/glibc227/lib -Wl,--dynamic-linker=/usr/local/glibc227/lib/ld-linux-x86-64.so.2 -lpthread
all: $(PROGRAMS)
clean:
rm -f $(PROGRAMS)
增加 gcc
的参数
-Wl,--rpath=/usr/local/glibc227/lib -Wl,--dynamic-linker=/usr/local/glibc227/lib/ld-linux-x86-64.so.2
使得程序使用 我们编译好的 libc
来链接程序。
参考
https://blog.csdn.net/jefbai/article/details/47859335
tcache_dup
介绍
通过 free
2次同一个 chunk
, 使得可以让两个指针分配到同一块内存
代码
#include <stdlib.h>
#include <stdio.h>
#include <stdint.h>
int main() {
void* p1 = malloc(0x40);
free(p1);
free(p1);
printf("Next allocated memory will be same: %p %p\n", malloc(0x40), malloc(0x40));
}
- 通过
_int_free
的源码我们知道, 在free
的时候,会检测p
的下一个chunk
(next
) 的PREV_INUSE
位 - 然后如果
tcache
指定项没有满就把chunk
加入tcache
- 但是如果
chunk
被放入了tcache
,next->PREV_INUSE
位不会被修改 ,所以还是会标志为in_used
. 所以我们可以 多次释放同一个chunk
.
所以我们释放两次 p1
, 此时 tcache
里面 size
为 0x50
( chunk
大小) 的项中就有 两个 一样 chunk
然后分配两次一样大小的 chunk
, malloc
会先用 tcache
分配,就会拿到两个一样的 chunk
01:38 haclh@ubuntu:tcache_pwn $ ./tcache_dup
Next allocated memory will be same: 0x602260 0x602260
可以看到分配到了两个地址一样的 chunk
.
tcache_house_of_spirit
介绍
通过伪造 size
,然后 free
掉这个 伪造的 chunk
, 然后再分配 size
大小的 chunk
, 就可以分配到指定位置。
代码
首先看看源代码
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
int main(int argc, const char* argv[]) {
size_t fake_chunk_and_more[64];
memset(fake_chunk_and_more, 'A', sizeof(fake_chunk_and_more));
printf("stack buf: %p\n", (void *)fake_chunk_and_more);
char* fake_chunk = (char * )fake_chunk_and_more;
*(long *)(fake_chunk + sizeof(long)) = 0x110;
*(long *)(fake_chunk + 0x110 + sizeof(long)) = 0x40; // 设置 pre_inused 位
char *mem = fake_chunk + 2*sizeof(long);
free(mem);
void *mem2 = malloc(0x100);
printf("malloc(0x100) returned: %p\n", mem2);
return 0;
}
就是在栈上面(用栈只是为了方便)伪造了 一个 0x110
大小 chunk
,
然后把它释放掉,他就会进入 tcache
,然后分配 0x110
的 chunk
就可以 分配到 fake_chunk_and_more
的地址
可以看到分配到了fake_chunk_and_more
.
调试过程的内存状态
熟悉 malloc
管理机制的老哥们可以比较奇怪,这里把 next_chunk->pre_inused = 0
( size = 0x40
) 。
在 源码里面是有通过 check_inuse_chunk
检测是否 double free
的 代码的
_int_free (mstate av, mchunkptr p, int have_lock)
{
size = chunksize (p);
....................................................
....................................................
check_inuse_chunk(av, p);
#if USE_TCACHE
{
size_t tc_idx = csize2tidx (size); // tcache bin 的索引
if (tcache
&& tc_idx < mp_.tcache_bins // 64 ,最多 64 个 bin
&& tcache->counts[tc_idx] < mp_.tcache_count) // 7 ,tcache->counts 存放每个 bin 已经存放的 chunk数量
{
tcache_put (p, tc_idx);
return;
}
}
#endif
但是从 ida
里面去看,居然不见了,校验 chunk
的 size
和 指针 后就直接进入 tcache
的处理的流程, 于是这里就算设置 下一个chunk
的 next_chunk->pre_inuse = 0
,也不会出现 crash
。
overlapping_chunks_by_caching
介绍
overlapping_chunks
这种技术非常经典了, 不过在 tcache
里面就非常的简单了, 修改 chunk
的 size
为 fake_size
, 然后 free
掉它,就会进入 fake_size
对应的 tcache
, 然后在 分配 fake_size
的 chunk
就可以拿到这个 chunk
, overlap chunk
代码
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
int main(int argc, const char* argv[]) {
char *mem = malloc(0x48);
char *sentry = malloc(0x18);
memset(sentry, 'b', 0x10);
printf("mem: %p, sentry: %p\n",mem, sentry);
printf("sentry content: %s\n", sentry);
*(long* )(mem - sizeof(long)) = 0x110; // 设置 chunk->size = 0x110
free(mem);
char *mem2 = malloc(0x100); // 分配一个 0x110 的chunk
memset(mem2, 'a', 0x100);
printf("mem2: %p\n", mem2);
printf("sentry content: %s\n", sentry);
return 0;
}
通过修改 mem
所在 chunk
的 size
为 0x110
然后释放掉他 ,然后分配一个 0x110
的 chunk
,我们就会再次分配到它。此时 mem2
的 chunk
包含了 sentry
的 chunk
调试过程
可以看到 mem2
所在 chunk
的大小为 0x110
, 而 sentry
和 mem2
相距 0x50
于是可以通过 mem2
修改 sentry
的内容
tcache_poisoning
介绍
通过修改 free
状态的 tcache
里面的 chunk
的 fd
(其实就是 tcache_entry->next
) ,可以分配到任意地址
代码
#include <malloc.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
int main(int argc, const char* argv[]) {
size_t target[6];
printf("stack: %p\n",target);
char *mem = malloc(0x48);
free(mem);
*(long *)(mem) = (long)target;
char *mem1 = malloc(0x48);
char *mem2 = malloc(0x48);
printf("mem2: %p\n", mem2);
return 0;
}
分配一个 0x50
的 chunk
然后释放它,进入 tcache
,然后修改 fd
为 target
然后分配两次 0x50
的 chunk
就可以分配到 target
成功分配到了 栈上面。
其实 fd
为任意地址都行,原因在于 tcache_get
直接从 tcache->entries
里面拿 chunk
, 而不检查 拿到的 chunk
是否合法。
同时 在 malloc
分配内存时,首先使用 tcache
,而它判断 tcache
有没有可以分配的 chunk
, 是直接判断指定项有没有指针。
DIAG_PUSH_NEEDS_COMMENT;
if (tc_idx < mp_.tcache_bins
&& tcache
&& tcache->entries[tc_idx] != NULL) // 根据tcache->entries[tc_idx]是否为空判断是否有chunk
{
return tcache_get (tc_idx);
}
DIAG_POP_NEEDS_COMMENT;
#endif
总结
tcache
的引入使得 heap
相关的漏洞的利用非常的简单了。
简单的原因主要在于 tcache
里面没有做什么检查, 同时还会优先使用这使得原来 malloc
里面的 check
也没有了作用。
free
的话 释放内存如果大小在 tcache
的范围内, 只检测 size 和 指针 是否合法,而且检测非常弱。
malloc
时 也是优先使用 tcache
, 只要 tcache->entries[tc_idx]
非空就可以从 tcache
分配。