前言 glibc 2.26
开始引入了 tcache
, 相关的 commit 可以看https://sourceware.org/git/p=glibc.git;a=commitdiff;h=d5c3fafc4307c9b7a4c7d5cb381fcdbfad340bcc 。 加入 tcache
对性能有比较大的提升,不过由于 tcache
的存在 ,一些利用方式的限制条件就少了许多。具体往下看。
相关文件位于 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_perthread_struct;
tcache
也是使用 类似 bins 方式来管理tcache
。
tcache_perthread_struct
是整个tcache
每一项由 相同大小的 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 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]); } 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 size_t tbytes; checked_request2size (bytes, tbytes); size_t tc_idx = csize2tidx (tbytes); MAYBE_INIT_TCACHE (); DIAG_PUSH_NEEDS_COMMENT; if (tc_idx < mp_.tcache_bins && tcache && tcache->entries[tc_idx] != NULL ) { return tcache_get (tc_idx); } 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 #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) { mchunkptr tc_victim; while (tcache->counts[tc_idx] < mp_.tcache_count && (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; if (av != &main_arena) set_non_main_arena (victim); check_malloced_chunk (av, victim, nb); #if USE_TCACHE size_t tc_idx = csize2tidx (nb); if (tcache && tc_idx < mp_.tcache_bins) { mchunkptr tc_victim; 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)) { .................... .................... .................... unsorted_chunks (av)->bk = bck; bck->fd = unsorted_chunks (av); #if USE_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 } ....................................... ....................................... ....................................... #if USE_TCACHE ++tcache_unsorted_count; if (return_cached && mp_.tcache_unsorted_limit > 0 && tcache_unsorted_count > mp_.tcache_unsorted_limit) { return tcache_get (tc_idx); } #endif ....................................... ....................................... ....................................... } #if USE_TCACHE 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); if (tcache && tc_idx < mp_.tcache_bins && tcache->counts[tc_idx] < mp_.tcache_count) { 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 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
同时在 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
这也使得很多安全检测不会被执行。
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
可以看到分配到了两个地址一样的 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 ; 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); if (tcache && tc_idx < mp_.tcache_bins && tcache->counts[tc_idx] < mp_.tcache_count) { 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 ; free (mem); char *mem2 = malloc (0x100 ); 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
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 ) { 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
https://www.cnblogs.com/hac425/p/9416796.html