尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

ctf-wiki ptmalloc2 堆实现深度解析:从堆初始化到内存块的申请与释放

发布时间:2026/9/28 2:55:51

资讯中心
01
ARTICLE

ctf-wiki ptmalloc2 堆实现深度解析:从堆初始化到内存块的申请与释放

ctf-wiki ptmalloc2 堆实现深度解析:从堆初始化到内存块的申请与释放
文档网络安全教程【免费下载链接】ctf-wikiCome and join us, we need you!项目地址https://gitcode.com/gh_mirrors/ct/ctf-wiki点击查看免费下载导读本文以 ctf-wiki 中 深入理解堆的实现 为核心骨架系统梳理 glibc ptmalloc2 堆分配器的完整实现脉络。文章首先确立宏观——堆生命周期、微观——块级操作的分析框架然后结合仓库中同目录下的 堆初始化、malloc_state 相关函数、申请内存块、释放内存块、基础操作 与 tcache 等源码级文档逐层展开malloc/free背后的内核调用链。读完本文你将掌握 ptmalloc2 从 arena 初始化、chunk 各 bin 分类到 fastbin / small bin / large bin / unsorted bin 分配与释放、前后向合并、sysmalloc扩容的完整机制并理解这些底层细节对堆漏洞挖掘unlink、fastbin attack、tcache attack 等的意义。理解堆实现的两个基本视角仔细思考会发现任何堆的实现都需要从以下两个角度考虑相应的问题宏观角度堆生命周期创建堆为进程建立可用的堆区域堆初始化建立管理结构初始化各 bin 链表删除堆堆区域收缩或归还系统微观角度块级操作申请内存块从堆中切出一块满足请求大小的 chunk释放内存块把不再使用的 chunk 归还给分配器当然这些都是比较高层面的想法不同的堆的底层实现会有所不同。对 glibc 而言宏观角度对应malloc_state/ arena 的建立与扩展微观角度对应__libc_malloc/__libc_free及其核心实现_int_malloc/_int_free。ctf-wiki 的 implementation 目录正是沿着这两个视角组织的heap-init.md与malloc-state.md回答堆如何诞生malloc.md与free.md回答块如何流转。宏观角度堆的创建与初始化初始化入口malloc_consolidate malloc_init_state堆初始化发生在用户第一次申请内存时具体路径是先执行malloc_consolidate再执行malloc_init_state详见 堆初始化 与 malloc_state 相关函数。malloc_init_state(mstate av)负责把一个malloc_state结构体初始化成可用状态其核心工作包括建立普通 bin 的循环链表对i 1; i NBINS; i把每个 bin 的fd与bk均指向自身空链表自环这是后续所有 bin 操作的基础。设置连续性与 fastbin 上限非主分配区非main_arena标记为不连续set_noncontiguous对main_arena调用set_max_fast(DEFAULT_MXFAST)设置 fastbin 的最大值。置位 FASTCHUNKS_BIT通过av-flags | FASTCHUNKS_BIT标记目前没有 fast chunk该标志会被后续的have_fastchunks/set_fastchunks/clear_fastchunks等宏使用。设置 top chunkav-top initial_top(av)即把unsorted bin头初始化为 top chunk此时 top 大小记录为 0表示堆尚未真正扩展。malloc_consolidate初始化与 fastbin 合并的二合一malloc_consolidate有两个功能取决于global_max_fast即get_max_fast()是否为 0若get_max_fast() ! 0fastbin 已初始化说明本次调用目的是合并 fastbin 中的 chunk。流程为先clear_fastchunks(av)清空 fastbin 标记然后从fastbin(av, 0)到fastbin(av, NFASTBINS - 1)按 fd 顺序遍历每个 fastbin把 bin 内每一个 chunk 取出来若物理低地址相邻 chunk 空闲!prev_inuse(p)则后向合并并unlink低地址 chunk若nextchunk ! av-top且高地址相邻 chunk 空闲!inuse_bit_at_offset(nextchunk, nextsize)则前向合并并unlink否则清除nextchunk的 prev_inuse 位以表明当前 fast chunk 可以合并合并后的 chunk 被插入unsorted bin 头部unsorted_bin-fd p; first_unsorted-bk p若是 large 范围则把fd_nextsize/bk_nextsize置空若nextchunk av-top则直接并入 top chunkav-top p。这里可以观察到 ptmalloc 的一个关键设计合并后的 chunk 不直接进入普通 bin而是先放到 unsorted bin 中等待下一次 malloc 重用从而避免在 malloc 尚未确定 chunk 是否会被立即复用时过早计算 bin。若get_max_fast() 0fastbin 未初始化说明 arena 尚未初始化此时调用malloc_init_state(av)完成初始化并在调试构建下执行check_malloc_state(av)做一致性校验。宏观扩展sysmalloc 与堆的扩容当 top chunk 也无法满足请求时_int_malloc会调用sysmalloc向操作系统申请更多内存详见 申请内存块 的sysmalloc小节。其核心逻辑若没有可用 arenaav NULL或请求大小nb mp_.mmap_threshold且 mmap 数量未达上限则优先走mmap 路径DEFAULT_MMAP_THRESHOLD为 128 KBpagesize默认为 4096 0x1000把 mmap 区域构造为带IS_MMAPPED标记的 chunk。对main_arena先计算size nb mp_.top_pad MINSIZEDEFAULT_TOP_PAD为 131072 字节即 0x20000若堆连续则扣除旧 top 大小并对齐到页大小后调用MORECORE(size)即 sbrksbrk 失败时以 mmap 兜底。扩容成功后更新av-system_mem/av-max_system_mem必要时把旧的 top chunk 收缩为 fencepost伪 chunk标记 inuse 且过小无法使用并释放剩余部分最终从新 top 上切出用户所需内存。微观角度之一申请内存块入口封装__libc_mallocglibc 源码中并没有一个名为malloc的实现函数用户调用的malloc真正落到的是__libc_malloc它只是对核心函数_int_malloc的简单封装详见 申请内存块首先通过atomic_forced_read(__malloc_hook)检查是否存在内存分配钩子若存在则直接调用钩子返回——这便于用户自定义堆分配函数进行测试。注意用户申请的字节一旦进入申请内存函数就变成了无符号整数。通过arena_get(ar_ptr, bytes)寻找一个可用的 arena随后调用victim _int_malloc(ar_ptr, bytes)。若分配失败且存在 arena则通过arena_get_retry换一个 arena 重试一次。退出前解锁ar_ptr-mutex并通过assert校验分配结果要么没分配到内存要么是 mmap 内存要么申请到的内存必须在其所分配的 arena 中。最后返回victim。核心分配器_int_malloc 的分层决策_int_malloc是内存分配的核心函数其核心思路可概括为四层递进由快到慢、由小到大根据用户申请的内存块大小以及相应大小 chunk 通常的使用频度fastbin chunk、small chunk、large chunk依次实现不同的分配方法由小到大依次检查不同的 bin 中是否有相应的空闲块可以满足用户请求当所有空闲 chunk 都无法满足时考虑 top chunk当 top chunk 也无法满足时才向系统申请内存块。进入函数后先checked_request2size(bytes, nb)把用户请求大小转换为内部 chunk 大小加上SIZE_SZ开销并做对齐保证至少MINSIZE同时拦截溢出回绕的超大请求。之后的分层路径如下① arena 为空av NULL时直接sysmalloc(nb, av)从 mmap 拿一块内存。② fastbin 路径最快若nb get_max_fast()注意这里比较的是无符号整数通过fastbin_index(nb)得到 bin 下标从 fastbin 头结点开始用catomic_compare_and_exchange_val_acq原子地取出链表头随后校验fastbin_index(chunksize(victim)) ! idx以防伪造否则报malloc(): memory corruption (fast)经chunk2mem转换并alloc_perturb填充后返回。③ small bin 路径若in_smallbin_range(nb)取victim last(bin)bin 的 bk 指向的最后一个 chunk。若victim bin说明 bin 为空若victim 0说明 small bin 尚未初始化需先malloc_consolidate(av)合并 fastbin否则校验bck-fd ! victim防伪造报malloc(): smallbin double linked list corrupted随后set_inuse_bit_at_offset标记使用、把 victim 从 bin 尾部摘下bin-bk bck; bck-fd bin非 main_arena 时置NON_MAIN_ARENA位后返回。small bin 每个大小对应一个 bin因此是精确匹配无需搜索。④ large bin 路径触发合并当 fastbin、small bin 都无法满足时idx largebin_index(nb)若存在 fast chunkhave_fastchunks(av)则先malloc_consolidate(av)。这里值得特别注意large bin 并没有直接去扫描对应 bin 中的 chunk而是先合并 fastbin 中的 chunk 放入 unsorted bin再在后续大循环中处理。为什么这是 ptmalloc 的机制——它会在分配 large chunk 之前对堆中碎片 chunk 进行合并以减少堆碎片。⑤ 大循环遍历 unsorted bin程序执行到这里说明与 chunk 大小正好一致的 binfastbin、small bin中没有 chunk 能直接满足需求。大循环按FIFO 方式、遍历顺序为 bk即从 unsorted bin 尾部开始逐个取出 chunk主要做三件事small request 的 last remainder 特例若请求为 small bin chunk、unsorted bin 中唯一一块是av-last_remainder且size nb MINSIZE注意没有等号则直接切分remainder chunk_at_offset(victim, nb)更新av-last_remainder把 remainder 重新挂回 unsorted binset_foot(remainder, remainder_size)记录剩余大小后返回 victim。exact fit若size nb说明大小正好合适通常是把刚合并出来的恰好合适的 chunk 分配出去直接set_inuse_bit_at_offset后返回。否则放回对应 binsmall 范围放回 small binlarge 范围则按fd_nextsize 递减排序插入 large bin——若新 chunk 比 bin 中最小的还小则直接插尾部若与已有 chunk 大小相同则插在相同大小 chunk 的后面且不修改 nextsize 指针降低开销只有遇到新的大小才维护fd_nextsize/bk_nextsize双向链表。插入完成后再mark_bin(av, victim_index)更新 binmap。while 循环最多迭代 10000 次MAX_ITERS后退出。⑥ large request 扫描当前 bin若!in_smallbin_range(nb)对bin_at(av, idx)用 skip listbk_nextsize反向遍历找第一个不小于nb的 chunk跳过空 bin 与最大 chunk 都太小的 bin若取到的 chunk 与下一个 chunk 大小相同则改取后者以避免调整 nextsize 链表随后unlink取出若remainder_size MINSIZE则整块耗尽否则切分并把 remainder 插入 unsorted bin。⑦ binmap 扫描更大 bin若当前 bin 无法满足idx后借助binmap每个 block 为 32 个 bit一个 bit 表示对应 bin 是否有空闲 chunk快速跳过空 bin从bit map时跳到下一个非空 block找到第一个可用的更大 bin取victim last(bin)bin 中最大 chunkunlink后切分small 范围 remainder 会被标记为av-last_remainder。⑧ 使用 top chunk若所有 bin 都无法满足use_top取出av-top。若size nb MINSIZE则直接切分并把 remainder 设为新 top若have_fastchunks(av)则先malloc_consolidate(av)合并 fastbin恢复idx后进入下一轮大循环重试否则堆内存不够调用sysmalloc向系统申请内存。微观角度之二释放内存块入口封装__libc_free与 malloc 类似free也有封装__libc_free详见 释放内存块检查__free_hook钩子存在则调用并返回free(NULL)无任何效果直接返回p mem2chunk(mem)把用户指针转换为 chunk 指针若chunk_is_mmapped(p)则走 munmap 路径若启用了动态阈值!mp_.no_dyn_threshold且 chunk 大小落在(mp_.mmap_threshold, DEFAULT_MMAP_THRESHOLD_MAX]区间会上调mp_.mmap_threshold并把mp_.trim_threshold设为它的 2 倍随后munmap_chunk(p)归还否则ar_ptr arena_for_chunk(p)找到归属 arena调用_int_free(ar_ptr, p, 0)。核心释放器_int_free 的分支处理_int_free先取size chunksize(p)随后做分层处理轻量级安全检查校验指针非非法地址且对齐p (uintptr_t)-size或misaligned_chunk(p)时报free(): invalid pointer校验size MINSIZE且aligned_OK(size)报free(): invalid size。① fastbin 路径若size get_max_fast()默认TRIM_FASTBINS为 0因此chunk 紧邻 top的排除条件不生效把 chunk 插入 fastbin 头部成为对应 fastbin 链表的第一个 free chunk。期间校验下一 chunk 大小合法否则报free(): invalid next size (fast)、free_perturb填充内存、set_fastchunks(av)置位通过原子 CAScatomic_compare_and_exchange_val_rel完成插入并检查链表头是否等于待插入 chunk 以阻止 double free报double free or corruption (fasttop)同时校验 fastbin 头 chunk 大小索引一致报invalid fastbin entry (free)。注意只有不是 fastbin 的情况才会触发 unlink。② 非 mmap chunk 的合并unlink 的舞台合并是为了避免堆中碎片过多合并顺序为先考虑物理低地址空闲块后考虑物理高地址空闲块合并后的 chunk 指向合并 chunk 的低地址。具体流程轻量检测待释放 chunk 不能是 topdouble free or corruption (top)、下一 chunk 不能越过 arena 边界double free or corruption (out)、下一 chunk 的 prev_inuse 必须为 1double free or corruption (!prev)、下一 chunk 大小合法free(): invalid next size (normal)。后向合并合并低地址 chunk若!prev_inuse(p)取prevsize prev_size(p)p chunk_at_offset(p, -prevsize)回退到低地址相邻空闲 chunk执行unlink(av, p, bck, fwd)。前向合并合并高地址 chunk若nextchunk ! av-topnextchunk空闲则unlink后并入否则清除nextchunk的 prev_inuse 位合并结果插入unsorted bin 头部bck-fd p; fwd-bk plarge 范围则置fd_nextsize/bk_nextsize为 NULL最后set_head/set_foot。若nextchunk av-top则直接把 chunk 并入 topav-top p。向系统返还内存若合并后size FASTBIN_CONSOLIDATION_THRESHOLD存在 fast chunk 就先malloc_consolidate对main_arena若 top 大于mp_.trim_threshold则调用systrim收缩MORECORE_CANNOT_TRIM未定义时对非主分配区则调用heap_trim收缩 heap。③ mmap chunk直接munmap_chunk(p)归还。贯穿全程的基础操作unlink 宏与 malloc_printerrunlink双向链表摘除unlink 用来把一个双向链表只存储空闲 chunk中的一个元素取出来是堆实现中使用非常频繁的原语因此被实现为宏详见 基础操作可能出现在malloc从恰好大小合适的 large bin 获取 chunk注意 fastbin 与 small bin 不使用 unlink这正是漏洞常出现在它们这里的原因依次遍历处理 unsorted bin 时也不使用 unlink从更大的 bin 中取 chunk。free后向合并低地址空闲 chunk、前向合并高地址空闲 chunk除 top 外。malloc_consolidate后向合并与前向合并。realloc前向扩展合并高地址空闲 chunk除 top 外。unlink 宏的关键逻辑与安全检查#define unlink(AV, P, BK, FD) { \ if (__builtin_expect (chunksize(P) ! prev_size (next_chunk(P)), 0)) \ malloc_printerr (corrupted size vs. prev_size); \ FD P-fd; \ BK P-bk; \ if (__builtin_expect (FD-bk ! P || BK-fd ! P, 0)) \ malloc_printerr (check_action, corrupted double-linked list, P, AV); \ else { \ FD-bk BK; \ BK-fd FD; \ if (!in_smallbin_range (chunksize_nomask (P)) \ __builtin_expect (P-fd_nextsize ! NULL, 0)) { \ if (__builtin_expect (P-fd_nextsize-bk_nextsize ! P, 0) \ || __builtin_expect (P-bk_nextsize-fd_nextsize ! P, 0)) \ malloc_printerr (check_action, \ corrupted double-linked list (not small), \ P, AV); \ if (FD-fd_nextsize NULL) { \ if (P-fd_nextsize P) \ FD-fd_nextsize FD-bk_nextsize FD; \ else { \ FD-fd_nextsize P-fd_nextsize; \ FD-bk_nextsize P-bk_nextsize; \ P-fd_nextsize-bk_nextsize FD; \ P-bk_nextsize-fd_nextsize FD; \ } \ } else { \ P-fd_nextsize-bk_nextsize P-bk_nextsize; \ P-bk_nextsize-fd_nextsize P-fd_nextsize; \ } \ } \ } \ }以 small bin 的 unlink 为例large bin 的 unlink 类似只是多了 nextsize 的处理可以看出P 最后的 fd 和 bk 指针并没有发生变化但当遍历整个双向链表时已经遍历不到该节点。这个特性很有用可以借此泄漏地址泄漏 libc 地址P 位于双向链表头部时泄漏 bk位于尾部时泄漏 fd链表只含一个空闲 chunk 时 fd、bk 均可泄漏。泄漏堆地址双向链表包含多个空闲 chunkP 位于头部泄漏 fd位于中间 fd、bk 均可泄漏位于尾部泄漏 bk。注意这里的头部指 bin 的 fd 指向的 chunk最新加入尾部指 bin 的 bk 指向的 chunk最先加入。同时无论 fd/bk 还是 fd_nextsize/bk_nextsize程序都会校验双向一致性防止攻击者简单篡改空闲 chunk 的 fd 与 bk 实现任意写——详细的利用手法可参见 unlink 利用。另外注意堆的第一个 chunk 所记录的 prev_inuse 位默认为 1。malloc_printerr错误处理glibc malloc 检测到错误时会调用malloc_printerrstatic void malloc_printerr(const char *str) { __libc_message(do_abort, %s\n, str); __builtin_unreachable(); }它调用__libc_message执行abortif ((action do_abort)) { if ((action do_backtrace)) BEFORE_ABORT(do_abort, written, fd); /* Kill the application. */ abort(); }在 glibc 2.23 版本中abort会先fflush(NULL)冲洗所有流因为用户可能为 SIGABRT 注册了处理函数。tcache性能与安全的取舍tcache 是 glibc 2.26Ubuntu 17.10之后引入的技术目的是提升堆管理性能但在提升性能的同时舍弃了很多安全检查也因此催生了大量新的利用方式详见 tcache。tcache 引入两个新结构体tcache_entry只含一个next指针链接空闲 chunk注意其 next 指向 chunk 的 user data而 fastbin 的 fd 指向 chunk 开头地址与tcache_perthread_struct每个线程一个含char counts[TCACHE_MAX_BINS]与tcache_entry *entries[TCACHE_MAX_BINS]TCACHE_MAX_BINS为 64。基本工作方式第一次 malloc 时先 malloc 一块内存存放tcache_perthread_structtcache_init()通过_int_malloc申请后memset清零。free 且 size 小于 small bin size 时优先放入对应 tcache 链直到填满默认tcache_count为 7 个填满后再次 free 的内存才像以前一样进入 fastbin 或 unsorted bintcache 中的 chunk 不会合并不取消 inuse bit。malloc 且 size 在 tcache 范围内时先从 tcache 取 chunk直到 tcache 为空为空后若fastbin/smallbin/unsorted bin中有符合大小的 chunk会先把 bin 中的 chunk 搬进 tcache 填满再从 tcache 取因此 chunk 在 bin 与 tcache 中的顺序会反过来。源码层面__libc_malloc中MAYBE_INIT_TCACHE()保证首次使用时初始化 tcache随后tcache_get()仅做取头、counts 减一几乎没有任何保护_int_free中若tcache tc_idx mp_.tcache_bins tcache-counts[tc_idx] mp_.tcache_count则进入tcache_put()直接插到链表头部也几乎没有任何保护且没有把 chunk 数据清零。tcache 的分配优先级高于 fastbinfastbin 的申请在未进入 tcache 流程之后。这种几乎无保护的特性正是 tcache attack 等利用技术的温床。调试支持perturb_byte 填充仓库中 测试支持 记录了perturb_byte机制默认值为 0仅用于测试辅助static int perturb_byte; static void alloc_perturb(char *p, size_t n) { if (__glibc_unlikely(perturb_byte)) memset(p, perturb_byte ^ 0xff, n); } static void free_perturb(char *p, size_t n) { if (__glibc_unlikely(perturb_byte)) memset(p, perturb_byte, n); }当通过mallopt(M_PERTURB, value)设置该参数后malloc 返回的内存会被填充为perturb_byte ^ 0xfffree 的内存会被填充为perturb_byte用于在调试中检测未初始化读取与 use-after-free。这也是在前文_int_malloc/_int_free中反复出现的alloc_perturb/free_perturb调用的由来。小结两条主线串起的完整机制回顾 ctf-wiki 的 implementation 框架可以把 ptmalloc2 的堆实现压缩成两条主线宏观主线进程第一次 malloc 时触发malloc_consolidate → malloc_init_state完成 arena 初始化各 bin 自环、top 指向 initial_toptop 不足时由sysmalloc通过 sbrk/mmap 扩容或新建堆释放大块时通过systrim/heap_trim收缩归还系统。微观主线__libc_malloc/__libc_free作为钩子友好的薄封装把核心工作交给_int_malloc/_int_free_int_malloc按 fastbin → small bin → large bin先 consolidate→ unsorted bin 大循环 → binmap 扫描 → top chunk → sysmalloc 的层级递进_int_free按 fastbin 直接插入或触发前后向合并unlink并最终进入 unsorted bin / top chunkunlink宏与malloc_printerr贯穿始终既是正确性保证也是漏洞利用如 unlink、fastbin attack的关键攻防点。理解这套宏观 微观的实现框架是深入阅读 glibc malloc 源码、分析堆漏洞以及撰写堆利用 PoC 的起点。更进一步可以继续在仓库中研读 堆结构总览、fastbin attack、unsorted bin attack 与 large bin attack 等章节把这些底层机制转化为实战能力。赞分享文档网络安全教程【免费下载链接】ctf-wikiCome and join us, we need you!项目地址https://gitcode.com/gh_mirrors/ct/ctf-wiki点击查看免费下载相关推荐CTF-Wiki 堆利用入门基石Linux ptmalloc2 堆管理器与堆内存分配原理全解CTF Wiki 堆利用入门基石Linux ptmalloc2 堆管理器与堆内存分配原理全解 堆Heap是 Linux 下程序动态内存分配的核心区域也是文档网络安全教程CTF-Wiki 堆利用基础深入剖析 glibc ptmalloc2 的 perturb_byte 内存填充机制CTF Wiki 堆利用基础深入剖析 glibc ptmalloc2 的 perturb_byte 内存填充机制 perturb_byte 是 glibc p文档网络安全教程GCS Bucket Architect 技能深入解析Phase 3 基于用户意图的输出生成与 Bucket 安全交付GCS Bucket Architect 技能深入解析Phase 3 基于用户意图的输出生成与 Bucket 安全交付 本文围绕 skills29/skill文档网络安全教程上一篇Nim标准库终极指南200模块完整功能解析与最佳实践下一篇Triton矩阵乘法优化如何实现10倍性能提升的终极指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。