tcache结构体(上)

admin 2026-01-28 06:56:27 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文详细解析了glibc2.27中tcache堆管理机制的核心原理与操作。内容涵盖tcache_entry及tcache_perthread_struct结构体定义,阐述了内存分配时tcache优先于fastbin的查找顺序,以及内存释放优先填充tcache的机制。文末附带战队招新信息。 综合评分: 81 文章分类: 二进制安全,CTF,漏洞分析


cover_image

tcache结构体(上)

原创

zach0ry zach0ry

OnePanda-Sec

2026年1月27日 16:04 陕西

20

25

OnePanda-Sec

tcache结构体(上)

原理与核心操作

招新说明

招新要求

· 热爱网络安全,喜欢CTF

· 拥有CTF比赛经验,有较好比赛成绩的

· 乐于奉献、热爱分享,愿意提升   自己同时帮助他人

· 时间允许参加各类赛事,服从战队管理与安排

· 各类比赛获奖者、能力出众者视情况考量

· 未参与其他高校联队

· 大一同学视情况放宽资历要求

联系方式

发送简历于邮箱

· 简历邮箱:[email protected]

01

tcache结构体

OnePanda-Sec

在 glibc 2.27 里,chunk 被 free 后如果满足大小范围且对应 bin 未满,就会按对齐后的 size class 进入该线程的 tcache:每个 size class 对应一个 tcache bin,bin 里只存同一对齐尺寸的 chunk,并用单链表管理;

具体做法是把 freed chunk 的用户区前 16 字节改写为 tcache_entry,其中前 8 字节存 next 指针、后 8 字节存 key(用于简单的 double free 检测),然后采用头插法把它挂到 tcache->entries[idx] 上,因此整体呈 LIFO(后进先出)顺序供后续同尺寸 malloc 优先取出。

tcache _entry

在 tcache 中新增了两个结构体,分别是 tcache_entry 和 tcache_perthread_struct:

typedef struct tcache_entry{  struct tcache_entry *next;//fd,下一个大小相同的空闲tcache chunk} tcache_entry;

这里的next指针指向的是user data部分,也就是说指向的是chunk head+0x10地址处,而fastbin则指向的是chunk head

tchache struct

typedef struct tcache_perthread_struct{  char counts[TCACHE_MAX_BINS];//每个tcache bin链的chunk个数  tcache_entry *entries[TCACHE_MAX_BINS];} tcache_perthread_struct;static __thread tcache_perthread_struct *tcache = NULL;

收藏了所有的tcache_entry

这个管理结构会在第一次调用malloc时,先malloc一块内存用来存放tcache_perthread_struct。因此做glibc >=2.26的题时,gdb的时候第一个堆块即为tcache_perthread_struct。

tcache_get

static __always_inline void *tcache_get (size_t tc_idx){&nbsp; tcache_entry *e = tcache->entries[tc_idx];//tc_idx 是要取的 tcache bin 下标(对应某个 size&nbsp;class)&nbsp;&nbsp;assert&nbsp;(tc_idx&nbsp;<&nbsp;TCACHE_MAX_BINS);//malloc的size合理&nbsp; assert (tcache->entries[tc_idx] >&nbsp;0);//不是 NULL&nbsp; tcache->entries[tc_idx] = e->next;//取出第一个e,头节点指向往后移&nbsp; --(tcache->counts[tc_idx]);&nbsp;&nbsp;return&nbsp;(void *) e;}

在 tcache_get 中,仅仅检查了 tc_idx ,此外,我们可以将 tcache 当作一个类似于 fastbin 的单独链表,只是它的 check,并没有 fastbin 那么复杂,仅仅检查 tcache->entries[tc_idx] = e->next;

tcache_put

static&nbsp;void&nbsp;*tcache_get&nbsp;(size_t&nbsp;tc_idx){&nbsp; tcache_entry *e = tcache->entries[tc_idx];&nbsp;&nbsp;assert&nbsp;(tc_idx < TCACHE_MAX_BINS);&nbsp;&nbsp;assert&nbsp;(tcache->entries[tc_idx] >&nbsp;0);&nbsp; tcache->entries[tc_idx] = e->next;&nbsp; --(tcache->counts[tc_idx]);&nbsp;&nbsp;return&nbsp;(void&nbsp;*) e;}

02

tcache bin 使用

OnePanda-Sec

malloc 查找/分配总体流程

1.对请求大小做对齐与最小化

  • 用户请求 bytes
  • 计算对齐后的 nb(实际 chunk size,含头部、对齐到 0x10,且 ≥ MINSIZE)

1.先查 tcache(线程私有缓存)

  • 如果 nb 在 tcache 支持范围内,算 tc_idx

  • 若 tcache->counts[tc_idx] > 0

  • 直接 tcache_get(tc_idx) 弹出表头并返回

  • tcache miss 才继续往下

1.若是小块,查 fastbin(arena 级)

  • 条件:nb <= get_max_fast()

  • 算 idx = fastbin_index(nb)

  • 若 fastbin[idx] 非空:

  • 弹出一个 victim

  • 校验 victim 的真实 size 是否属于当前 idx

  • 顺便把 fastbin 里剩下的同尺寸 chunk 回填进 tcache(refill)

  • 返回 victim

1.查 smallbin(精确大小 bins)

  • 如果 nb 属于 smallbin 范围
  • 直接看对应 smallbin 是否有空闲 chunk
  • 有则取出(FIFO/双向链表)

1.处理 unsorted bin(未分类大杂烩)

  • 先遍历 unsorted:

  • 若有大小正好/足够的 chunk

  • 可能直接切割返回

  • 不合适的会被分流进 smallbin/largebin

1.查 largebin(大块,按范围分组)

  • 找到最合适的(best-fit)
  • 可能切割返回

1.都没有就从 top chunk 切

  • top 足够大 → 切一块返回,剩下继续当 top

1.top 也不够就向系统要内存

  • sbrk 扩展 heap 或 mmap 直接映射大块
  • 生成新 top 或直接返回 mmap 块

一句话版顺序

tcache → fastbin → smallbin → unsorted → largebin → top → sysmalloc(sbrk/mmap)

内存申请

(1)首先,申请的内存块符合 fastbin 大小时并且在 fastbin 内找到可用的空闲块时,会把该 fastbin 链上的其他内存块放入 tcache 中。

(2)其次,申请的内存块符合 smallbin 大小时并且在 smallbin 内找到可用的空闲块时,会把该 smallbin 链上的其他内存块放入 tcache 中。

(3)当在 unsorted bin 链上循环处理时,当找到大小合适的链时,并不直接返回,而是先放到 tcache 中,继续处理。

贴个符合 fastbin 的时候

//tcachebin不满足时&nbsp;&nbsp;if&nbsp;((unsigned&nbsp;long) (nb) <= (unsigned&nbsp;long) (get_max_fast&nbsp;()))//判断是否属于 fastbin 范围&nbsp; &nbsp; {//nb 是当前 malloc 需要的 chunk size(对齐后的大小)。&nbsp; &nbsp; &nbsp; idx =&nbsp;fastbin_index&nbsp;(nb);&nbsp; &nbsp; &nbsp; mfastbinptr *fb = &fastbin&nbsp;(av, idx);&nbsp; &nbsp; &nbsp; mchunkptr pp;&nbsp; &nbsp; &nbsp; victim = *fb;//fastbin不为空&nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;(victim !=&nbsp;NULL)&nbsp; &nbsp; {&nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;(SINGLE_THREAD_P)&nbsp; &nbsp; &nbsp; &nbsp; *fb = victim->fd;&nbsp; &nbsp; &nbsp;&nbsp;else&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;REMOVE_FB&nbsp;(fb, pp, victim);&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;//校验size的合理性&nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;(__glibc_likely (victim !=&nbsp;NULL))&nbsp; &nbsp; &nbsp; &nbsp; {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;size_t&nbsp;victim_idx =&nbsp;fastbin_index&nbsp;(chunksize&nbsp;(victim));&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;(__builtin_expect (victim_idx != idx,&nbsp;0))//实际size和malloc计算的下标校验&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;malloc_printerr&nbsp;("malloc(): memory corruption (fast)");&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;check_remalloced_chunk&nbsp;(av, victim, nb);#if&nbsp;USE_TCACHE&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;/* While we're here, if we see other chunks of the same size,&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;stash them in the tcache. &nbsp;*/&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;//填补tcache_bin&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;size_t&nbsp;tc_idx =&nbsp;csize2tidx&nbsp;(nb);&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;(tcache && tc_idx < mp_.tcache_bins)//有tcache,范围合理&nbsp; &nbsp; &nbsp; &nbsp; {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; mchunkptr tc_victim;&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;/* While bin not empty and tcache not full, copy chunks. &nbsp;*/&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;while&nbsp;(tcache->counts[tc_idx] < mp_.tcache_count&& (tc_victim = *fb) !=&nbsp;NULL)&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;(SINGLE_THREAD_P)&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;*fb = tc_victim->fd;//从fastbin弹出&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;else&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; {&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;REMOVE_FB&nbsp;(fb, pp, tc_victim);&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;(__glibc_unlikely (tc_victim ==&nbsp;NULL))&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;break;&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;tcache_put&nbsp;(tc_victim, tc_idx);//放入tcache&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; }&nbsp; &nbsp; &nbsp; &nbsp; }#endif&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;void&nbsp;*p =&nbsp;chunk2mem&nbsp;(victim);&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;alloc_perturb&nbsp;(p, bytes);&nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;p;&nbsp; &nbsp; &nbsp; &nbsp; }&nbsp; &nbsp; }&nbsp; &nbsp; }

unsoreted bin扫描

在 glibc 2.27 的 _int_malloc 遍历 unsorted bin 时,只有一类 chunk 会被“顺手塞进 tcache”:和当前申请大小同尺寸的小块

开启mp_.tcache_unsorted_limit > 0(提前返回)

#if&nbsp;USE_TCACHE&nbsp; &nbsp; &nbsp;&nbsp;/* If we've processed as many chunks as we're allowed while&nbsp; &nbsp;filling the cache, return one of the cached ones. &nbsp;*/&nbsp; &nbsp; &nbsp; ++tcache_unsorted_count;//为了给 tcache 回填,在 unsorted bin 里已经处理了多少个 chunk&nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;(return_cached&nbsp; &nbsp; &nbsp; &nbsp; && mp_.tcache_unsorted_limit >&nbsp;0&& tcache_unsorted_count > mp_.tcache_unsorted_limit)&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;//有返回的chunk以及扫描到达最大的限度了会停&nbsp; &nbsp; &nbsp; {&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;tcache_get (tc_idx);&nbsp; &nbsp; &nbsp; }#endif没有开启#if&nbsp;USE_TCACHE&nbsp; &nbsp; &nbsp;&nbsp;/* If all the small chunks we found ended up cached, return one now. &nbsp;*/&nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;(return_cached)&nbsp; &nbsp; &nbsp; {&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;tcache_get (tc_idx);&nbsp; &nbsp; &nbsp; }#endif

内存释放

_int_free (mstate av, mchunkptr p,&nbsp;int&nbsp;have_lock){&nbsp; INTERNAL_SIZE_T size; &nbsp; &nbsp; &nbsp; &nbsp;/* its size */&nbsp; mfastbinptr *fb; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;/* associated fastbin */&nbsp; mchunkptr nextchunk; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;/* next contiguous chunk */&nbsp; INTERNAL_SIZE_T nextsize; &nbsp; &nbsp;/* its size */&nbsp;&nbsp;int&nbsp;nextinuse; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;/* true if nextchunk is used */&nbsp; INTERNAL_SIZE_T prevsize; &nbsp; &nbsp;/* size of previous contiguous chunk */&nbsp; mchunkptr bck; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;/* misc temp for linking */&nbsp; mchunkptr fwd; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;/* misc temp for linking */&nbsp; size =&nbsp;chunksize&nbsp;(p);&nbsp;&nbsp;/* Little security check which won't hurt performance: the&nbsp; &nbsp; &nbsp;allocator never wrapps around at the end of the address space.&nbsp; &nbsp; &nbsp;Therefore we can exclude some size values which might appear&nbsp; &nbsp; &nbsp;here by accident or by "design" from some intruder. &nbsp;*/&nbsp;&nbsp;if&nbsp;(__builtin_expect ((uintptr_t) p > (uintptr_t) -size,&nbsp;0)&nbsp; &nbsp; &nbsp; || __builtin_expect (misaligned_chunk&nbsp;(p),&nbsp;0))&nbsp; &nbsp;&nbsp;malloc_printerr&nbsp;("free(): invalid pointer");&nbsp;&nbsp;/* We know that each chunk is at least MINSIZE bytes in size or a&nbsp; &nbsp; &nbsp;multiple of MALLOC_ALIGNMENT. &nbsp;*/&nbsp;&nbsp;if&nbsp;(__glibc_unlikely (size < MINSIZE || !aligned_OK&nbsp;(size)))&nbsp; &nbsp;&nbsp;malloc_printerr&nbsp;("free(): invalid size");&nbsp;&nbsp;check_inuse_chunk(av, p);#if&nbsp;USE_TCACHE&nbsp; {&nbsp; &nbsp;&nbsp;size_t&nbsp;tc_idx =&nbsp;csize2tidx&nbsp;(size);&nbsp; &nbsp;&nbsp;if&nbsp;(tcache&nbsp; &nbsp; &nbsp; && tc_idx < mp_.tcache_bins&nbsp; &nbsp; &nbsp; && tcache->counts[tc_idx] < mp_.tcache_count)&nbsp; &nbsp; &nbsp; {&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;tcache_put&nbsp;(p, tc_idx);&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return;&nbsp; &nbsp; &nbsp; }&nbsp; }#endif......}

0nePandaSec团队交流群

欢迎网络安全爱好者加入

OnePanda-Sec


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:OnePanda-Sec zach0ry zach0ry《tcache结构体(上)》

评论:0   参与:  0