-
-
[原创]pwnable.tw新手向write up(七) Tcache tears-Tcache attack初探
-
发表于: 2020-5-19 21:18 9437
-
从Glibc 2.26版本开始,新引入了一个tcache的技术,旨在提升内存分配的速度,但是提升速度的时候舍弃了很多检查,因此导致了很多新的安全问题.接下来以2.27版本的源码稍微分析一下.
tcache_entry用单向链表来链接Tcache结构,next指针指向下一个相同大小的chunk.与fast bin不同的是,next指向user data,而不是指向chunk的开头.
tcache_perthread_struct是整个Tcache的管理结构.
counts用来记录entires中每一项当前链入的chunk数目,最多可以有七个chunk.
entires保存了64项tcache_entry,也就是说一个管理结构最多可以拥有64个tcache_entry.
tcache_put函数首先把chunk的user data部分转化为一个tcache_entry *类型,然后插入到tcache->entries[tc_idx]的头部,然后把tcache->counts[tc_idx]加一,用来表示新增了一个chuank到结构体中.
tcache_get函数从结构体中取出一个tcache,将他以void *的形式返回,并且把tcache->counts[tc_idx]的值减一,用来表示结构体中少了一个chunk.
首先根据传入的参数bytes计算所需chuank的实际大小,然后计算Tcache对应的下标tc_idx.
如果是第一次malloc的话,会调用MAYBE_INIT_TCACHE ()来对Tcache进行初始化.初始化之后,如果下标tc_idx没有越界,并且tcache->entries[tc_idx]不为空的话,就会调用tcache_get (tc_idx)来获取Tcache中的一块chunk.
上述条件不成立的话,程序才会进入常规的内存分配中,这里就可以看出Tcache的优先级非常高,比fast bin还要更早被分配.
tcache_init()成功执行之后,tcache_perthread_struct就被成功建立了.
在相应大小的fast bin找到chunk之后,就把chunk从fast bin中拿出来.然后把相应大小的fast bin中剩下的chunk全部加入Tcache,直到放满为止(也就是满七个为止).最后返回最开始那个满足条件的chunk.
如果fast bin不满足分配的话,接着进入small bin的分配:
和fast bin基本一样.在相应大小的small bin找到chunk之后,就把chunk从small bin中拿出来.然后把相应大小的small bin中剩下的chunk全部加入Tcache,直到放满为止(也就是满七个为止).最后返回最开始那个满足条件的chunk.
如果small bin不满足分配的话,接着进入unsorted bin的分配:
在遍历unsorted bin的时候,如果有大小适配的chunk,不会立刻返回,而是将这个chunk加入Tcache,并且设置return_cached=1,表示Tcache此时已经有了符合大小的chunk.如果大小不是正好适配,那么就把chunk放到相应的bin中.
遍历完unsorted bin之后,根据return_cached的值来判断Tcache里面是否有符合大小的chunk,如果有的话直接调用tcache_get (tc_idx)并返回获得的chunk.如果没有的话则继续使用large bin或者top chunk来分配.
首先获取要释放的chunk的size,然后判断size是否符合规范,如果符合的话就看tcache->counts[tc_idx]有没有满,没满的话就直接放入Tcache,然后返回,
如果上述条件没有成立的话,就按照常规方法来释放chunk.
Tcache有64个bin数组,所以在64位的情况下,Tcache可以接受0×20~0×410大小的chunk.当我们分配这个范围内的chunk时,不会直接进入bin,而是在Tcache中分配内存,释放的时候也是同理,但是Tcache自身基本没什么检测,所以我们可以轻易实现好多在bin中很难进行的操作.
Tcache在free的时候并不会修改in_use位,所以我们可以连续释放同一个chunk来造成Double free,这个比fast bin还不安全.
运行结果为:
这个其实挺恐怖的,只要修改空闲Tcache的next指针,直接就能分配任意地址了.
运行结果为:
可以回顾一下tcache_get函数,拿chunk出来的时候并不检查地址是否合法,malloc分配的时候,也只是简单的判断了一下tcache->entries[tc_idx]是否为空.所以就造成了任意地址分配
比以前的house of spirit更简单了,只需伪造一个size区域,然后将伪造的fake chunk释放,再次malloc相应大小就可以得到fake_chunk.
运行结果:
很轻松的实现了伪造chunk的分配.
main函数很常规,要注意的有两点,用来存放我们姓名的name_space,还有就是题目的漏洞点,释放chunk的时候并没有将指针置0,导致了UAF.
申请chunk,并用ptr来保存mem指针.这里其实也有一个溢出的漏洞,但是并没有用到.
更短小了,打印name_space的内容,可以考虑信息泄露.
利用思路
exp
blog:0x2l's Blog
#if USE_TCACHE /* We overlay this structure on the user-data portion of a chunk when the chunk is stored in the per-thread cache. */ typedef struct tcache_entry { struct tcache_entry *next; } tcache_entry;
# define TCACHE_MAX_BINS 64 typedef struct tcache_perthread_struct { char counts[TCACHE_MAX_BINS]; tcache_entry *entries[TCACHE_MAX_BINS]; } tcache_perthread_struct;
/* Caller must ensure that we know tc_idx is valid and there's room for more chunks. */ 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]); }
/* Caller must ensure that we know tc_idx is valid and there's available chunks to remove. */ 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; }
void * __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); size_t tc_idx = csize2tidx (tbytes); 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) { 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);
static void tcache_init(void) { mstate ar_ptr; void *victim = 0; const size_t bytes = sizeof (tcache_perthread_struct); if (tcache_shutting_down) return; // 找到可用的 arena arena_get (ar_ptr, bytes); // 申请一个 sizeof(tcache_prethread_struct) 大小的 chunk victim = _int_malloc (ar_ptr, bytes); if (!victim && ar_ptr != NULL) { ar_ptr = arena_get_retry (ar_ptr, bytes); victim = _int_malloc (ar_ptr, bytes); } if (ar_ptr != NULL) __libc_lock_unlock (ar_ptr->mutex); /* In a low memory situation, we may not be able to allocate memory - in which case, we just keep trying later. However, we typically do this very early, so either there is sufficient memory, or there isn't enough memory to do non-trivial allocations anyway. */ // 初始化Tcache if (victim) { tcache = (tcache_perthread_struct *) victim; memset (tcache, 0, sizeof (tcache_perthread_struct)); } }
static void * _int_malloc (mstate av, size_t bytes) { 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 /* While we're here, if we see other chunks of the same size, 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. */ 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; } } }
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 /* While we're here, if we see other chunks of the same size, 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; } }
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
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
[注意]传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!