尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

深入解析 ptmalloc2 堆溢出:从漏洞原理到利用思路(CTF-Wiki)

深入解析 ptmalloc2 堆溢出:从漏洞原理到利用思路(CTF-Wiki) 文档网络安全教程【免费下载链接】ctf-wikiCome and join us, we need you!项目地址https://gitcode.com/gh_mirrors/ct/ctf-wiki点击查看免费下载堆溢出Heap Overflow是 CTF 二进制利用中最基础也最核心的漏洞类型之一。本文以 CTF-Wiki 仓库中 docs/zh-tw/docs/pwn/linux/user-mode/heap/ptmalloc2/heapoverflow-basic.md 为主体骨架结合仓库内 ptmalloc2 堆结构、malloc 实现与 unlink 等攻击技法的文档系统讲解堆溢出的成因、检测方法与利用策略。读完本文你将掌握如何识别堆溢出漏洞、如何计算精确的溢出填充长度以及如何基于 glibc 的 chunk 结构与 bin 机制将溢出转化为任意地址读写与控制程序执行流。什么是堆溢出堆溢出是指程序向某个堆块chunk中写入的字节数超过了该堆块本身可使用的字节数从而导致数据溢出覆盖到物理相邻的高地址的下一个堆块。这里需要特别强调一个关键概念之所以说是可使用而不是用户申请的字节数是因为堆管理器会对用户申请的字节数进行内部调整这也导致实际可利用的字节数不小于用户申请的字节数。这一调整逻辑体现在 glibc 源码的request2size宏中见下文确定填充长度一节。堆溢出漏洞发生的基本前提只有两点程序向堆上写入数据写入的数据大小没有被良好地控制。对于攻击者来说堆溢出漏洞轻则可以使程序崩溃DoS重则可以使攻击者完全控制程序的执行流程。堆溢出与栈溢出的本质差异堆溢出是一种特定的缓冲区溢出除此之外还有栈溢出、bss 段溢出等。但与栈溢出不同的是堆上并不存在返回地址等可以让攻击者直接控制执行流程的数据因此我们一般无法直接通过堆溢出来控制 EIP指令指针。一般来说利用堆溢出的策略分为两条路线覆盖与其物理相邻的下一个 chunk 的内容包括prev_size前一个 chunk 的大小仅当前一个 chunk 空闲时有效sizechunk 的大小其中低三位比特位与真实大小共同决定布局——NON_MAIN_ARENA0x4标记 chunk 是否不属于主线程 arenaIS_MAPPED0x2标记 chunk 是否由 mmap 分配PREV_INUSE0x1标记前一个物理相邻 chunk 是否处于使用状态the True chunk sizechunk 的真实大小chunk content用户数据从而改变程序固有的执行流。利用堆管理机制如 unlink 等实现任意地址写入Write-Anything-Anywhere或控制堆块中的内容从而控制程序的执行流。这两个策略分别对应了直接覆盖元数据与利用堆算法逻辑两类攻击范式前者为后者的铺垫覆盖 size、fd/bk 指针正是触发 unlink 等机制的前提。更系统的堆背景知识可参考仓库中的 堆概述heap-overview.md 与 堆相关数据结构heap-structure.md。基本示例溢出覆盖 top chunk下面通过一个简单程序演示堆溢出的发生过程#include stdio.h int main(void) { char *chunk; chunkmalloc(24); puts(Get input:); gets(chunk); return 0; }这个程序的主要目的是调用malloc分配一块堆上的内存之后向这个堆块中写入一个字符串。如果输入的字符串过长会导致溢出 chunk 的区域并覆盖到其后的 top chunk 之中实际上puts内部会调用 malloc 分配堆内存覆盖到的可能并不是 top chunk。溢出前的堆内存布局64 位系统0x602000: 0x0000000000000000 0x0000000000000021 chunk 0x602010: 0x0000000000000000 0x0000000000000000 0x602020: 0x0000000000000000 0x0000000000020fe1 top chunk 0x602030: 0x0000000000000000 0x0000000000000000 0x602040: 0x0000000000000000 0x0000000000000000其中0x21即 chunk 的 size 字段0x20为对齐后的 chunk 大小低位的0x1是PREV_INUSE位0x20fe1是 top chunk 的大小。当我们执行print A*100进行写入后0x602000: 0x0000000000000000 0x0000000000000021 chunk 0x602010: 0x4141414141414141 0x4141414141414141 0x602020: 0x4141414141414141 0x4141414141414141 top chunk(已被溢出) 0x602030: 0x4141414141414141 0x4141414141414141 0x602040: 0x4141414141414141 0x4141414141414141可以看到A已经覆盖了相邻的 top chunk 区域。在本例中由于溢出的数据没有继续被堆管理器校验程序可能只是崩溃但若被覆盖的是某个已释放 chunk 的fd/bk指针或 size 字段就可能引发 unlink 等更严重的后果。堆溢出利用的三步分析法堆溢出利用中比较重要的几个步骤可归纳为寻找堆分配函数 → 寻找危险函数 → 确定填充长度。第一步寻找堆分配函数通常来说堆是通过调用 glibc 函数malloc进行分配的。在某些情况下会使用calloc分配。calloc与malloc的区别是calloc 在分配后会自动进行清空清零这对于某些信息泄露漏洞的利用来说是致命的因为申请到的内存中不会残留前序数据也就无法通过打印未初始化数据来泄露信息calloc(0x20); //等同于 ptrmalloc(0x20); memset(ptr,0,0x20);除此之外还有一种分配是经由realloc进行的。realloc函数可以身兼malloc和free两个函数的功能#include stdio.h int main(void) { char *chunk,*chunk1; chunkmalloc(16); chunk1realloc(chunk,32); return 0; }realloc的操作并不是像字面意义上那么简单其内部会根据不同情况执行不同操作当realloc(ptr,size)的size不等于ptr的 size 时如果申请的size 原来的 size如果 chunk 与 top chunk 相邻直接扩展这个 chunk 到新 size 大小如果 chunk 与 top chunk 不相邻相当于free(ptr); malloc(new_size)。如果申请的size 原来的 size如果相差不足以容得下一个最小 chunk64 位下 32 字节32 位下 16 字节则保持不变如果相差可以容得下一个最小 chunk则切割原 chunk 为两部分free 掉后一部分。当realloc(ptr,size)的size等于 0 时相当于free(ptr)当realloc(ptr,size)的size等于ptr的 size 时不进行任何操作。从源码层面看glibc 中并不存在名为malloc的函数用户调用的malloc实际是封装了_int_malloc的__libc_malloc。它会先检查__malloc_hook钩子再通过arena_get获取分配区最后调用_int_malloc完成分配若分配失败还会用arena_get_retry重试其他 arena。详见仓库中的 implementation/malloc.md。第二步寻找危险函数通过寻找危险函数我们可以快速确定程序是否可能有堆溢出以及如果有的话堆溢出的位置在哪里。常见的危险函数如下输入类gets直接读取一行忽略\x00不检查长度是最高危的输入函数scanf、vscanf格式化输入%s同样不做长度限制输出类sprintf向缓冲区格式化输出不检查目标缓冲区大小字符串类strcpy字符串复制遇到\x00停止strcat字符串拼接遇到\x00停止bcopy内存拷贝不检查目标大小。这些函数共同的特点是不接收或未正确使用目标缓冲区的容量参数。在逆向分析时可重点检索对这些函数的调用点并结合其参数判断溢出是否可控、溢出偏移是多少。第三步确定填充长度这一部分主要是计算我们开始写入的地址与所要覆盖的地址之间的距离它直接决定了溢出 payload 的布局。一个常见的误区是认为 malloc 的参数等于实际分配堆块的大小。但事实上ptmalloc 分配出来的大小是对齐的。这个对齐长度一般是字长的 2 倍例如 32 位系统是 8 字节64 位系统是 16 字节。对于不大于 2 倍字长的请求malloc 会直接返回 2 倍字长的块即最小 chunk。例如 64 位系统执行malloc(0)会返回用户区域为 16 字节的块#include stdio.h int main(void) { char *chunk; chunkmalloc(0); puts(Get input:); gets(chunk); return 0; }其内存布局如下根据系统位数malloc 会分配 8 或 16 字节的用户空间//根据系统的位数malloc会分配8或16字节的用户空间 0x602000: 0x0000000000000000 0x0000000000000021 0x602010: 0x0000000000000000 0x0000000000000000 0x602020: 0x0000000000000000 0x0000000000020fe1 0x602030: 0x0000000000000000 0x0000000000000000注意用户区域的大小不等于chunk_head.size。二者满足关系chunk_head.size 用户区域大小 2 * 字长即 chunk 头部 prev_size 与 size 两个字段的大小。借用的 prev_sizechunk 间的空间复用之前提到的用户申请的内存大小会被堆管理器修改还体现在另一个机制上——申请到的 chunk 有可能会使用与其物理相邻的下一个 chunk 的 prev_size 字段来储存内容。回顾前面的示例#include stdio.h int main(void) { char *chunk; chunkmalloc(24); puts(Get input:); gets(chunk); return 0; }我们申请的 chunk 大小是 24 字节。但将其编译为 64 位可执行程序时实际上分配的可用空间是 16 字节而不是 24 字节0x602000: 0x0000000000000000 0x0000000000000021 0x602010: 0x0000000000000000 0x0000000000000000 0x602020: 0x0000000000000000 0x0000000000020fe116 字节的空间如何装得下 24 字节的内容答案是借用了下一个块的 prev_size 域。因为根据 chunk 结构知识prev_size字段仅当物理相邻的前一块处于释放状态时才起作用当前一块在使用状态时该字段可以被前一块当作数据空间复用。结合 16 字节的 user data 与下一个 chunk 的 8 字节 prev_size恰好凑足 24 字节。request2size申请大小与实际大小的转换用户申请的内存大小与 glibc 实际分配的内存大小之间的转换由request2size宏完成定义于 glibc 的 malloc.c仓库的 heap-structure.md 中有完整引用/* pad request bytes into a usable size -- internal version */ //MALLOC_ALIGN_MASK 2 * SIZE_SZ -1 #define request2size(req) \ (((req) SIZE_SZ MALLOC_ALIGN_MASK MINSIZE) \ ? MINSIZE \ : ((req) SIZE_SZ MALLOC_ALIGN_MASK) ~MALLOC_ALIGN_MASK)当req24时request2size(24)32。而除去 chunk 头部的 16 字节2 × SIZE_SZ实际上用户可用的 chunk 字节数为 16。再加上下一个 chunk 可复用的 prev_size 字段8 字节正好 24 字节。实际上ptmalloc 分配内存是以双字double word为基本单位的。以 64 位系统为例分配出来的空间是 16 的整数倍即用户申请的 chunk 都是 16 字节对齐的。与request2size配套的还有checked_request2size它先通过REQUEST_OUT_OF_RANGE判断请求大小是否会因填充对齐而发生回绕wrap around若溢出则返回ENOMEM错误。理解这一转换过程对计算溢出偏移至关重要——填充长度必须以实际可用的对齐后大小而不是 malloc 参数为基准来计算。源码视角为什么堆溢出如此危险malloc_chunk 结构被溢出者与溢出载体ptmalloc 中一切堆块都用统一的malloc_chunk结构表示无论其处于分配还是释放状态见 heap-structure.mdstruct malloc_chunk { INTERNAL_SIZE_T prev_size; /* Size of previous chunk (if free). */ INTERNAL_SIZE_T size; /* Size in bytes, including overhead. */ struct malloc_chunk* fd; /* double links -- used only if free. */ struct malloc_chunk* bk; /* Only used for large blocks: pointer to next larger size. */ struct malloc_chunk* fd_nextsize; /* double links -- used only if free. */ struct malloc_chunk* bk_nextsize; };其中INTERNAL_SIZE_T默认为size_tSIZE_SZ sizeof(INTERNAL_SIZE_T)64 位系统为 8。相关宏包括chunk2mem(p)/mem2chunk(mem)chunk 头指针与用户指针互转偏移量为 2 × SIZE_SZMIN_CHUNK_SIZE最小 chunk 大小即offsetof(struct malloc_chunk, fd_nextsize)MINSIZE对齐后的最小可分配 chunkPREV_INUSE(0x1)、IS_MMAPPED(0x2)、NON_MAIN_ARENA(0x4)size 字段低三位的标志位chunksize(p)忽略 SIZE_BITS 后获取 chunk 真实大小next_chunk(p)获取物理相邻的下一个 chunk。堆溢出的威胁正源于此一旦溢出数据越过了 chunk 的 user data 边界第一个受害对象就是物理相邻下一个 chunk 的 headerprev_size 与 size若下一个 chunk 处于空闲状态并被放入 bin 链表其fd/bk乃至fd_nextsize/bk_nextsize也随之暴露在攻击者面前从而为 unlink、House of 系列等高级攻击提供入口。_int_malloc 分配流程理解溢出触发的上下文从 implementation/malloc.md 的源码分析可知_int_malloc是内存分配的核心函数其核心思路为根据用户申请的内存块大小以及相应大小 chunk 通常使用的频度fastbin / small / large chunk依次实现不同的分配方法由小到大依次检查不同的 bin 中是否有相应的空闲块可以满足用户请求当所有的空闲 chunk 都无法满足时考虑 top chunk当 top chunk 也无法满足时堆分配器才真正向系统申请内存。函数开头首先调用checked_request2size(bytes, nb)将用户请求转换为内部 chunk 大小。分配路径依次为fastbinnb get_max_fast()→ smallbinin_smallbin_range(nb)→ 合并 fastbins 后进入 unsorted bin 大循环 → large bin / top chunk / sysmalloc。理解这一流程对堆溢出利用的意义在于溢出后堆块布局的变化如伪造的 size、被篡改的 fd/bk会在下一次 malloc/free 时被堆管理器消费从而决定攻击的成败与触发时机。从堆溢出到 unlink任意地址写堆溢出最常见的进阶利用之一就是 unlink。其核心思想是通过溢出修改一个空闲 chunk 的fd和bk指针当堆管理器对该 chunk 执行 unlink双向链表摘除操作时就会发生两次受控的写入。仓库的 unlink.md 给出了详细原理古老的 unlink无检查时代FD P-fdBK P-bk随后执行FD-bk BK、BK-fd FD可直接实现任意地址写当前的 unlink带检查要求FD-bk P BK-fd P。通过精心构造 fakeFD 与 fakeBK 使*(fakeFD12)与*(fakeBK8)均指向同一个指向 P 的指针即可让该指针被改写为P - 0x18实现使指向 UAF chunk 的指针 ptr 变为 ptr - 0x18的效果进而篡改指针、获得任意读写。利用条件通常为存在 UAF可修改 free 状态下 smallbin 或 unsorted bin 的fd/bk指针已知位置存在一个指针指向可进行 UAF 的 chunk。这与本文覆盖物理相邻 chunk 的 size 与内容的策略完全衔接——堆溢出是制造这些伪造条件的最常见手段。相关衍生漏洞off-by-one 与 House 系列在掌握堆溢出基础后建议继续研读仓库中的关联章节off-by-one.md单字节溢出的原理与利用重点介绍了 NULL byte off-by-one通过溢出 NULL 字节清除PREV_INUSE位、伪造prev_size造成堆块重叠并给出了 Asis CTF 2016 b00ks、plaidctf 2015 plaiddb 等真实赛题分析unlink.md任意地址写的高级技法house-of-force.md 与 house-of-einherjar.md通过溢出伪造 top chunk size 或前块实现特殊分配fastbin-attack.md溢出篡改 fastbin 链表指针实现任意地址分配。实战利用路线小结综合上述分析一次典型的 ptmalloc2 堆溢出利用流程如下确定分配函数明确目标程序使用的是malloc、calloc还是realloc注意calloc的清零行为会阻碍信息泄露定位危险函数通过gets、strcpy、sprintf等危险调用点确定溢出位置与写入可控性计算填充长度依据request2size的对齐规则算出从写入起点到目标字段下一个 chunk 的 size / fd / bk的字节距离特别注意malloc(0)、malloc(24)这类请求实际可用空间大于参数值的空间复用情况选择利用策略目标字段是 size 位 → 考虑 House of Force、off-by-null 造成的堆重叠目标是空闲 chunk 的 fd/bk → 考虑 unlink、fastbin attack目标是函数指针 / hook 附近 → 在获得任意写后劫持__free_hook、__malloc_hook或 one_gadget验证与调试使用 gdb 观察堆内存布局如示例中的0x602000起始布局确认溢出偏移与写入效果再编写完整的 pwntools exploit。小结堆溢出是理解现代堆利用技术的基石。它的本质是写多了一个字节到写多了一整块的边界失控而 ptmalloc2 精心设计的 chunk 元数据prev_size/size/fd/bk与 bin 管理机制恰好为攻击者提供了将溢出转化为信息泄露、堆块重叠、任意地址写乃至代码执行的完整链路。掌握本文介绍的寻找分配函数 → 定位危险函数 → 计算填充长度三步法并结合 heap-structure.md 的宏定义与 implementation/malloc.md 的分配源码深入学习即可为后续的 unlink、House 系列、tcache 攻击等进阶主题打下坚实基础。赞分享文档网络安全教程【免费下载链接】ctf-wikiCome and join us, we need you!项目地址https://gitcode.com/gh_mirrors/ct/ctf-wiki点击查看免费下载相关推荐E7Helper 安装教程零基础把第七史诗的刷书签与讨伐交给脚本E7Helper 安装教程零基础把第七史诗的刷书签与讨伐交给脚本 凌晨一点讨伐还剩最后两把没刷商店的刷新倒计时又跳到了可点的状态——这种不做不甘心、做了文档网络安全教程ctf-wiki 栈溢出基础实战x86-32从 gets 漏洞到覆盖返回地址的完整利用流程ctf wiki 栈溢出基础实战x86 32从 gets 漏洞到覆盖返回地址的完整利用流程 本文基于 ctf wiki https://link.gitc文档网络安全教程3步解锁qBittorrent隐藏搜索功能一站式获取全网种子资源3步解锁qBittorrent隐藏搜索功能一站式获取全网种子资源 还在为寻找优质种子资源而烦恼吗search plugins项目为你带来了革命性的解决方案。文档网络安全教程上一篇GoCV安装终极指南5个常见问题快速解决下一篇Haystack 集成 Mirage用 MirageShellTool 为 Agent 打造统一虚拟文件系统 Shell创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表