PWN题解:PolarCTF easy_str的house of corrosion利用分析

发布时间:2026/9/9 8:53:33
PWN题解:PolarCTF easy_str的house of corrosion利用分析 打PolarCTF 2023冬季赛的时候我在这道 easy_str 上卡了挺久。题目标题里把“corrosion”拼成了“corrsion”一开始还以为是题目自己打的错别字结果后面才发现这题是真真切切的 house of corrosion只是名字写错了而已。整道题把格式化字符串和堆利用串在一起前面送一个明晃晃的格式化字符串漏洞后面藏着一个需要认真抠堆布局的 large bin 技巧属于那种“会者不难难者不会”的题。这篇文章就按我实际复现这道题的顺序来写先讲题目结构和漏洞点再拆解 house of corrosion 的核心原理然后给出完整的利用步骤和 exp 关键代码最后把我在调试中踩过的坑整理成速查表。适合有一定 PWN 基础、想搞懂 large bin attack 和 fastbin 攻击配合的选手如果你只是想找个能直接抄的 exp第三、四节可以直接看。1. 题目总览与考点定位1.1 赛题环境与基本信息这道题是 PolarCTF 2023冬季个人挑战赛的 PWN 方向题目难度定位在中等偏上。题目在远程环境跑在 glibc 2.27 上这一点非常关键因为 house of corrosion 这个技巧在 2.27 下最顺手到了 2.31 虽然还能用但需要额外处理一些检查到了 2.32 引入了 safe-linking 之后整个利用链复杂度会上升一大截。拿到文件先做基础检查保护情况如下64 位 ELF开启了 PIE开启了 NX栈不可执行开启了 Canary栈保护存在没有开启 Full RELROGOT 表可写从保护配置就能看出来题目并不打算让你走栈溢出那条路Canary 和 PIE 都开了常规的栈劫持基本是死路。正确的攻击面在堆上但是堆上又不像很多题目那样直接给你任意大小的 malloc而是限制得很死。这种“洞给了但路很窄”的设计反而是比赛题最喜欢考的点。1.2 程序结构还原与漏洞点分析我复现时用的交互流程大概是这样的程序启动后会先进入一个_menu函数这个函数会读取一段输入然后直接printf(buf)典型的格式化字符串漏洞。而且这个格式化字符串是在主流程最开始触发的只触发一次所以利用的时候必须在这一发之内把该泄露的信息全拿齐不然就只能靠堆上的手段去泄露。打完招呼之后进入主菜单提供增删改查四个功能add可以申请堆块但大小被限制成两种分别对应 0x20 和 0x80 的用户区大小edit编辑指定索引的堆块内容但是长度检查不严可以越界写入delete释放堆块但是释放后指针没有置空存在 UAFshow打印指定索引的堆块内容可以用来泄露堆上残留的数据组合起来看漏洞模型很清晰格式化字符串漏洞用于泄露 libc 基址、堆地址、canaryUAF 编辑越界用于篡改空闲堆块的指针字段堆大小虽然被限制但可以通过伪造 size 字段把多个小堆块合并成一个大 chunk从而制造出 large bin 需要的尺寸这里要注意一个细节add 函数虽然只让你申请两种大小但如果你申请的是 0x80 那个尺寸实际 malloc 出来的 chunk 用户区是 0x80chunk 头加用户区总共 0x90。这个尺寸离 large bin 的最低要求0x400 以上还差得远所以必须通过溢出改 size 来构造大 chunk。1.3 为什么是 house of corrosion很多人看到这个题的漏洞组合第一反应是打 unsorted bin attack 配合 fastbin attack 改__free_hook。但实际做下来会发现这个思路在这道题里很难走通原因有两个。第一个原因是 add 限制太死你申请不出大尺寸的 chunk 来打常规的 large bin attack必须自己伪造 size 造大 chunk这就让堆布局变得很别扭。第二个原因是程序只有一次格式化字符串泄露之后就没有第二次机会了如果利用链中间某一步出的偏差导致程序崩溃你只能重新连不能靠格式化字符串无限调试。house of corrosion 的优势在于它只需要一次精准的写操作来改global_max_fast之后整个 libc 的 data 段都变成 fastbin 的“猎场”可以在任意地址附近构造 fake chunk。这个技巧在 glibc 2.27 下非常稳定一旦global_max_fast被改大后面的利用就变得很宽裕。所以这道题本质上考查的是两件事第一你能不能把 house of corrosion 的原理搞清楚第二你能不能在一个受限的堆布局里把 large bin attack 和 unsorted bin attack 两次写操作精确地串起来。2. 核心技术house of corrosion 原理剖析2.1 global_max_fast 到底是什么在 glibc 的 malloc 实现里global_max_fast是一个位于 libc 数据段的全局变量它表示 fastbin 机制能处理的 chunk 最大 size。默认值是DEFAULT_MXFAST也就是 0x80。global_max_fast在 malloc 和 free 两个路径里都有判断逻辑free 的时候如果被释放 chunk 的 size 小于等于global_max_fast它会被直接放进 fastbin而不是进入 unsorted binmalloc 的时候如果请求的 size 小于等于global_max_fast分配器会优先去 fastbin 对应的链表里找空闲 chunk这个变量是一个全局值它存储的是 size 大小的上限而不是某个地址。但它有一个非常特殊的性质它本身的地址正好位于 libc 的 data 段和 main_arena 挨得很近。这就给了攻击者一个机会——如果能把一个很大的值写进去fastbin 的范围就会被无限放大。试想一下如果global_max_fast被改成了0x7fffffffffff这样的值那么任何小于这个值的 chunk 在 free 时都会进入 fastbin。但真正关键的不是这个而是 fastbin 的检查逻辑里有一个条件if (__glibc_unlikely (chunksize_nanomask (victim) ! size)) malloc_printerr (malloc(): memory corruption (fast));这个检查只在 fastbin 链表非空时才会校验 size。在 2.27 里fastbin 的 fd 指针是没有加密的也就没有 safe-linking 保护。所以你只要能在内存里伪造一个看起来像 fastbin chunk 的结构大小落在 fastbin 范围里分配器就会把它当作一个合法的空闲 chunk 返回给你。house of corrosion 的整个思路就是把global_max_fast改成一个巨大的值然后把 libc 数据段上那些“碰巧长得像 chunk”的内存利用起来最终实现对任意地址的分配。2.2 第一次写large bin attack 写 heap 地址到 global_max_fasthouse of corrosion 的第一跳是通过 large bin attack 把一个 heap 地址写到global_max_fast。这一步的目的是为了“垫一个值”让第二次写能通过安全检查。问题来了为什么需要先垫一个 heap 地址这就涉及到 unsorted bin attack 的检查条件了。在_int_malloc处理 unsorted bin 的循环中有这么一段代码bck victim-bk; if (__glibc_unlikely (bck-fd ! victim)) malloc_printerr (malloc(): corrupted unsorted chunks);这里victim是从 unsorted bin 取出的空闲 chunkbck是victim-bk指向的下一个 chunk。如果我们想利用 unsorted bin attack 往global_max_fast写入 main_arena 地址就需要把victim-bk改成一个特殊的目标地址同时保证检查通过。正常做法是把victim-bk改成global_max_fast - 0x10这样后续执行bck-fd unsorted_chunks(av)时就会在 global_max_fast -

相关新闻