资讯动态

House of Orange详解:无Free堆利用与FSOP攻击链

发布时间:2026/9/14 13:59:41 来源:尧图企业网站定制
我最早看House of Orange这道题时第一反应是这多半又是个unsorted bin的常规套路结果翻了十几篇writeup发现所有人都在反复摆弄一个叫OFILE的结构体。当时很困惑orange和文件结构体有什么关系后来把整个利用链走通了一遍才明白这其实是同一条流水线上的两道工序第一道是伪造top chunk来骗过malloc拿到unsorted bin第二道是借助OFILE结构体完成FSOPFile Stream Oriented Programming拿到shell。这两件事单独拆开都好理解但连在一起就成了很多人卡壳的坎。这篇文章我打算直接把这道坎拆平。我会先从OFILE结构体本身讲起把_IO_FILE_plus里那些我们真正用得上的字段、vtable机制、_IO_list_all的触发路径讲透然后回到House of Orange的第一阶段看看没有free函数的程序是怎么被malloc“帮忙”释放top chunk的接着讲最关键的一步——怎么在main_arena里“种”出一个能通过校验的伪FILE结构最后给出完整的FSOP收网思路、调试心得和不同glibc版本下的注意事项。如果你已经会基础的堆溢出和unsorted bin利用但一直对House of Orange这套组合拳似懂非懂这篇应该能帮你把整条链拼起来。1. 先定位OFILE结构体和House of Orange为什么会同时出现House of Orange这个名字来自HITCON 2016的heapstorm一题最初是为了解决“程序里完全没有free函数”这个难题。面对这种题常规的堆利用思路基本都失灵了因为你没法通过free把chunk送进bin里。攻击者想到的办法是既然不能主动free那就让malloc在内部帮我们free一次。实现方式是溢出修改top chunk的size让top chunk变得不够用然后malloc在尝试扩展堆时会触发一次内部的_int_free把旧top chunk释放到unsorted bin。等unsorted bin到手后面的路就宽了泄露libc地址、构造unsorted bin attack、甚至进一步打成FSOP。而在FSOP阶段攻击目标就不在堆上了而是libc中的_IO_list_all链表。我们要在内存中伪造一个_IO_FILE_plus结构体也就是平时逆向时IDA里显示的OFILE通过控制它的vtable指针让glibc在刷新文件流时调用到system。所以可以把两者的关系理解成House of Orange解决的是“如何在没有free的情况下进入unsorted bin”的问题而OFILE结构体是这条链后半段FSOP的载体。前半段拿不到unsorted bin后半段的FSOP无从谈起后半段不懂OFILE的各字段偏移和触发逻辑即使拿到了unsorted bin也只能停步于泄露地址。这两者不是二选一而是同一个攻击链上的前后两部分。也正是因为这种强耦合很多人在学House of Orange时会把注意力全放在第一段top chunk伪造上等到unsorted bin attack成功后对着_IO_list_all不知道该往哪改或者改了之后直接崩在_IO_flush_all_lockp里。问题几乎都出在对OFILE结构体不够熟、对FSOP触发时glibc内部做了什么不够清楚。这篇文章的顺序就是照着这条链来的先讲清楚结构体再讲如何伪造top chunk最后再把他们组装在一起。2. OFILE结构体里值得背下来的关键字段与触发机制2.1 从_IO_FILE到_IO_FILE_plusvtable才是真正的目标在glibc里文件流对象实际是一个_IO_FILE_plus结构体它由两部分组成一个_IO_FILE结构体和一个指向_IO_jump_t的vtable指针。_IO_FILE就是IDA逆向时常说的OFILE里面保存了文件描述符、读写缓冲区指针、锁指针等一堆状态信息。但对我们做漏洞利用来说大多数字段只是用来通过某些校验的“装饰”真正的目标是最后那个vtable指针。64位下_IO_FILE_plus的vtable指针位于偏移0xd8。vtable指向的_IO_jump_t是一张函数指针表里面按固定偏移存放着finish、overflow、underflow、xsputn、xsgetn、seekoff这些操作函数。其中_IO_OVERFLOW在vtable0x18的位置这也是FSOP里最常被替换成system的槽位。这个布局解释了为什么我们伪造的是OFILE而不是单纯改一个函数指针。直接改FILE结构体里的某个数据字段不会导致代码执行但只要能控制vtable指针再控制vtable0x18处的内容一旦glibc对这个文件流执行flush操作就会调用到我们指定的system函数。2.2 _IO_list_all和_IO_flush_all_lockp谁在什么时机遍历文件链表_IO_list_all是libc里的一个全局指针指向所有已打开文件流组成的链表头。正常情况下stdin、stdout、stderr都会挂在这条链表上。glibc在退出、异常终止、某些malloc错误处理路径中会调用_IO_flush_all_lockp来遍历这条链表准备刷新所有文件的缓冲区。_IO_flush_all_lockp的遍历逻辑大致是这样for (fp (_IO_FILE *) _IO_list_all; fp; fp fp-_chain) { // 检查是否需要调用 _IO_OVERFLOW }它会对每个节点做条件判断比较重要的是这几个_mode偏移0xc0是否小于等于0_IO_write_ptr偏移0x28是否大于_IO_write_base偏移0x20。如果这些条件满足就会通过该FILE结构体的vtable调用_IO_OVERFLOW。也就是说只要我们伪造的OFILE能通过这个checkglibc就会替我们调用vtable里的函数指针。出于安全考虑glibc在2.24版本中加入了IO_validate_vtable在调用vtable函数前会校验vtable指针是否落在libc的__libc_IO_vtables段内。所以在2.24及以后的版本里直接让vtable指向堆上的假vtable是行不通的。这也是为什么经典的直接伪造vtable方案基本都限定在2.23及以前的环境里2.24以后通常要改用_IO_str_jumps这类libc自带的合法vtable来做间接跳转。2.3 字段偏移速查表做FSOP时反复要用到的就是这几项64位glibc下_IO_FILE_plus的关键字段偏移如下字段偏移作用_IO_read_ptr0x08通常用不到但会被遍历代码读到_IO_write_base0x20与_write_ptr比较决定是否触发overflow_IO_write_ptr0x28触发条件的关键_chain0x68链表下一项用于跳转到下一个伪FILE_lock0x88部分路径会尝试加锁可置0防止崩溃_mode0xc0必须小于等于0才能进入flush分支vtable0xd8指向函数指针表FSOP核心目标做House of Orange的时候我一般会在gdb里用这条命令快速验证伪造是否到位gef p *(struct _IO_FILE_plus*)fake_file_addr或者手动按偏移查看关键字段gef x/gx fake_file_addr 0x20 gef x/gx fake_file_addr 0x28 gef x/gx fake_file_addr 0xd8先把这张偏移表刻在脑子里后面分析main_arena里怎么种伪FILE时你会反复用到这几个数字。3. 第一步伪造top chunk size让malloc帮我们完成一次“free”3.1 为什么修改top chunk的size就能触发内部释放House of Orange第一阶段的核心是把top chunk的size改成一个很小但对齐的值。经典的操作是把size改成0xc01。为什么是0xc01这里有几个硬性条件size的低4位要保留标志位所以常见写法是0xc01其中低位的1表示PREV_INUSE去标志后的实际大小0xc00必须大于MINSIZE否则后续_int_free会报invalid size这个大小必须小于用户下一次请求的nbMINSIZE这样malloc才会认为top chunk不够用走进sysmalloc。当程序再次malloc一个大块时_int_malloc在现有bin里找不到合适的chunktop chunk又不够切就会调用sysmalloc去扩展堆。sysmalloc在处理旧top chunk时会检查它是否满足释放条件。如果旧top chunk已经不在当前堆的扩展末端同时大小满足MINSIZE就会先调用_int_free把旧top chunk释放掉再向系统申请新的堆空间。这里有一个看似矛盾但实际很关键的点程序明明没有free函数攻击者也没有直接调用free然而glibc在内部替我们调用了_int_free。结果就是旧top chunk被放进了unsorted bin我们凭空获得了一个unsorted bin chunk而且它的地址就是原来top chunk的地址攻击者依然拥有这块内存的读写能力。这就是“malloc帮我们完成一次free”的本质。3.2 改完size之后接下来的malloc路径到底经历了什么我们来走一遍完整的路径。假设堆上top chunk原本是0x21000大小攻击者通过溢出把它改成0xc01。之后程序执行malloc(0x1000)_int_malloc首先在fastbin、smallbin、unsorted bin里找有没有合适的chunk没有走到use_top分支发现top chunk size是0xc00比请求的0x1000加上MINSIZE还小不够用如果此时没有fastbin chunk则不会触发malloc_consolidate直接进入sysmallocsysmalloc发现旧top chunk size满足MINSIZE且不是堆扩展末端于是把它的大小改成(0xc00-0x10)|PREV_INUSE调用_int_free释放释放后的旧top chunk进入unsorted bin成为一个size约为0xbf0的unsorted chunksysmalloc再向系统申请新内存建立新的top chunk。这个过程在glibc 2.23里是能稳定复现的。但到了2.29以后_int_malloc里对top chunk size的检查变得更严格查出了类似“malloc(): corrupted top size”的校验条件导致这种经典的top chunk伪造思路不再那么容易直接成立。这也是为什么House of Orange的题目多集中在老版本glibc上。3.3 触发这一步时的调试验证点如果你在自己环境里复现建议在malloc(0x1000)之前的gdb断点处检查几个值top chunk 地址 top chunk size应为0xc01 用户请求大小 MINSIZE用gdb可以看到类似这样的状态gef heap Chunk(addr0x602000, size0xc01, flagsPREV_INUSE)触发后再用unsorted bin状态确认旧top chunk是否成功进入unsorted bin。如果这一步失败先检查是不是size没满足对齐或者请求大小不足以让sysmalloc进入释放分支。4. 关键布局如何在main_arena里“种”出伪FILE结构4.1 为什么伪FILE结构非得放在main_arena里第一阶段成功之后我们有了一个可控的unsorted bin chunk再加上unsorted bin attack攻击者能把一个固定地址写入_IO_list_all。问题来了unsorted bin attack写入_IO_list_all的值是固定的它就是unsorted_chunks(av)的地址也就是main_arena内部靠前的一个位置。我们没法选择把它写成堆地址。因此_IO_list_all最终会指向main_arena内部。glibc在FSOP触发时会把_IO_list_all当作一个_IO_FILE_plus来解析也就是说伪FILE结构被迫建立在main_arena内部。我们必须在攻击触发前通过堆里的bin操作把main_arena特定偏移处的值变成可控的堆指针这样伪FILE的_write_base、_write_ptr、_chain这些字段才能满足条件。这就引申出一个关键技巧在main_arena里“种”smallbin。通过精心构造让某个smallbin桶的fd/bk位置正好落在伪FILE结构需要的字段偏移上这样main_arena里那片看起来完全不可控的内存就被我们用堆指针填充了。4.2 没有free函数怎么让chunk进入smallbin到了这一步很多人会陷入一个误区程序没有free函数我哪来的smallbin答案是不需要主动free让malloc替我们把unsorted bin里的chunk“整理”到smallbin。_int_malloc在处理unsorted bin时有一个很重要的行为当从unsorted bin取出的chunk既不能精确匹配、也不够切割时它会把这个chunk放入对应大小的smallbin或largebin然后继续处理下一个unsorted chunk。这个整理动作发生在malloc的内部我们只需要控制好请求的大小就能借malloc之手把某个小chunk种进smallbin。具体做法可以这样理解假设unsorted bin里有一个size为0x70的小chunk此时我们不去malloc一个0x70大小的请求那样会直接把它取走而是malloc一个0x400之类的大请求。这个请求肯定不是0x70的匹配项于是glibc在unsorted bin循环中会先把0x70的chunk放入smallbin再去largebin和top里满足这个0x400的请求。结果是0x70的chunk留在了smallbin桶里而且桶的fd/bk位置指向了这块堆内存。main_arena的smallbin区域就留下了可控的堆指针。这一步是整个House of Orange后半段里最需要掐准的地方。不同环境里需要根据_IO_list_all目标地址和OFILE字段偏移反推出该种哪个大小的smallbin、种在哪个桶。很多writeup里直接写“free两个0x60的chunk”那是在有条件free的题目里在house of orange场景下就是用这种“大请求迫使glibc整理unsorted bin”的方式把小chunk种进smallbin。4.3 一个具体的字段对准思路以64位2.23环境为例具体数值因libc版本有差异但思路一致。假设unsorted bin attack会把_IO_list_all写成unsorted_chunks(av)也就是main_arena里bin区的头部附近。伪FILE结构从这个地址开始解析0x20处是_IO_write_base需要从一个堆地址读到另一个比它更大的堆地址这样才能满足_IO_write_ptr _IO_write_base0x68处是_chain最好读到一个我们可控的堆chunk地址这样遍历能跳转到第二个伪FILE0xd8处是vtable这个位置太深了通常很难在main_arena里直接种出合适的堆地址所以经典打法往往是让_chain先跳走在堆上布置第二个完整伪FILE和假vtable。也就是说main_arena里那个“伪FILE”很多时候只是用来通过第一轮检查并跳转真正执行system的“干活”结构放在堆上。这就是为什么我们经常看到exp里构造了两层FILE结构第一层在main_arena里做跳板第二层在堆上做攻击。如果你在调试中看到_IO_flush_all_lockp触发了但一直崩在奇怪的位置大概率是第一层伪FILE的_chain没有正确指向可控堆地址或者是第二层伪FILE的vtable没有对准。5. 收网unsorted bin attack FSOP 执行system(/bin/sh)5.1 unsorted bin attack把unsorted_chunks(av)写进_IO_list_all我们先回顾一下unsorted bin attack的原理。在malloc从unsorted bin取出chunk时会有这样一段操作bck victim-bk; unsorted_chunks(av)-bk bck; bck-fd unsorted_chunks(av);如果我们把unsorted bin中某个chunk的bk改写成_IO_list_all - 0x10那么在unsorted bin unlink的过程中就会向bck-fd也就是_IO_list_all写入unsorted_chunks(av)的地址。于是_IO_list_all被改成了main_arena内部的一个地址。这个攻击只能写一次之后再继续malloc很容易破坏unsorted bin链表导致崩溃所以通常要把它放在最后一步先完成所有堆布局最后一次malloc触发写入紧接着程序执行exit或走入错误处理路径触发_IO_flush_all_lockp完成FSOP。5.2 触发FSOP的常见入口_IO_flush_all_lockp的触发点非常关键因为malloc成功返回后不会自动调用它。在CTF题目里触发FSOP的常见入口有这几个程序正常调用exit退出程序触发malloc_printerr比如检测到堆异常后abort程序调用abort、__stack_chk_fail等异常终止路径某些题目会显式调用fflush(NULL)刷新所有流。如果触发时机没把握住可能导致_IO_list_all已经改了但程序从头到尾没有进_IO_flush_all_lockp攻击就卡住了。所以做题时先确认程序退出路径是什么再决定何时触发unsorted bin attack。5.3 实际利用步骤的完整链路把整条链串起来看House of Orange的完整步骤是通过溢出修改top chunk size为0xc01触发一次大malloc让旧top chunk进入unsorted bin通过show等功能泄露unsorted bin chunk的fd/bk算出libc基址切割unsorted bin留下一个小chunk并利用“大请求迫使整理”把它种进smallbin改写某个unsorted bin chunk的bk为_IO_list_all - 0x10最后一次malloc触发unsorted bin attack把unsorted_chunks(av)写入_IO_list_all程序走exit或错误路径触发_IO_flush_all_lockp遍历伪FILE链调用system(/bin/sh)。第4步和第7步是最容易出问题的。第4步的问题在于smallbin桶的位置和OFILE字段的对准关系必须通过gdb实测确认第7步的问题在于vtable校验和_chain跳转如果第二层伪FILE没写好很容易在调用_IO_OVERFLOW前就崩掉。5.4 用gdb验证伪FILE链是否就绪在触发FSOP前我会用gdb手动模拟_IO_flush_all_lockp的遍历检查关键字段1. 查看_IO_list_all指向的地址 2. 检查该地址0x20与0x28处的值确认write_ptr write_base 3. 检查该地址0xc0处的_mode值确认不大于0 4. 检查该地址0x68处的_chain确认能跳转到可控堆地址 5. 检查第二层伪FILE的vtable0x18是否真的是system地址这五步逐项过完FSOP基本就能稳定触发。我自己的经验是第2步检查经常能发现write_base和write_ptr都是0这种情况不会调用overflow整条链就安静地走完了shell也拿不到。遇到这种问题就要回去调整smallbin的种植位置。6. 调试心得与glibc版本差异6.1 老版本环境怎么搭House of Orange的经典利用依赖glibc 2.23及以前的环境。想在国内的CTF题目里复现最省事的方式是用Ubuntu 16.04的docker镜像或者用patchelf把目标程序的动态链接器替换成libc-2.23.so再配合LD_PRELOAD加载对应版本的libc。我个人更推荐直接上docker因为配套的gdb调试环境干净不会因为本机glibc高版本导致各种莫名其妙的偏移问题。调试时用pwndbg或gef配合heap、bins、got这些命令效率会高很多。版本不同main_arena里bins的起始偏移、unsorted_chunks(av)的地址、_IO_list_all的相对偏移都可能有变化千万不要拿writeup里的绝对地址硬套一定要在自己环境里用gdb实测。6.2 2.24到2.27vtable校验来了假vtable方案要变glibc 2.24引入IO_validate_vtable后直接把vtable指向堆上的假vtable会在调用时崩溃因为校验要求vtable地址必须落在__libc_IO_vtables段内。这时候FSOP的常见替代方案是改用_IO_str_jumps或_IO_wstr_jumps这类libc自带的vtable。_IO_str_jumps是字符串流对应的vtable它的_IO_OVERFLOW槽位指向_IO_str_overflow。这个函数在内部会根据FILE结构体中的字段来操作缓冲区攻击者通过设置_IO_buf_base、_IO_buf_end等字段可以触发一次从任意地址到任意地址的复制或者让malloc走到__malloc_hook。不同glibc版本里的适配方式有细微差异但总体思路都是利用合法vtable内部逻辑来间接劫持控制流。6.3 2.29之后第一阶段本身也开始变得困难glibc 2.29以后_int_malloc对top chunk的校验变严。具体来说就是当使用top chunk时会检查size是否大于system_mem如果发现异常直接报“malloc(): corrupted top size”。虽然House of Orange把top size改小看起来不属于“大于system_mem”但版本迭代后sysmalloc释放旧top chunk的路径、_int_free的校验逻辑都有调整经典触发方式在实战中经常稳定复现不了。所以如果你的题目是2.31或者更高版本的libc最好先确认House of Orange第一阶段是否还能触发。有些新版本里可以通过其他方式伪造arena或使用large bin attack来达到类似的“无free获得unsorted bin”效果但那就是另一套知识体系了。6.4 常见报错与排查方向调试时最常遇到的几个报错和对应的排查方向我整理成了表格报错信息可能原因排查方向malloc(): memory corruptionunsorted bin chunk的size或fd/bk被改坏检查修改bk为_IO_list_all-0x10之前chunk的size和fd是否还合法free(): invalid pointer旧top chunk释放时检查没通过确认top chunk size是否满足MINSIZE、是否对齐以及该chunk是否确实不在堆扩展末端double free or corruption (!prev)释放top chunk时prev_inuse位或相邻chunk状态不对确认top chunk被改写的size里PREV_INUSE位是否为1Fatal error: glibc detected an invalid stdio handle伪FILE结构体校验失败用gdb检查_IO_list_all指向地址处的各字段是否按预期设置Segmentation fault in _IO_flush_all_lockpvtable或_chain指向了非法地址逐步检查两层FILE结构的vtable和_chain是否都指向可控内存最后再分享一个小技巧在unsorted bin attack触发之前一定先把所有必要的堆布局做完因为unsorted bin attack成功之后unsorted bin链表已经被破坏后续基本没法再调用malloc了。很多新手把unsorted bin attack放在前面写完之后又想着再malloc一次调整布局结果直接触发malloc_printerr整个利用变成一场空。这个顺序问题我踩过一次之后就再也没忘过。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价