尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
House of Orange:无free堆题中从top chunk劫持到FSOP的完整利用链
年初打某场线下赛被一道菜单题卡了一晚上add和show两个功能堆溢出给得明明白白唯独没有delete。没有free意味着我熟悉的unsorted bin attack根本找不到触发点——你没机会把一个正常chunk释放进bin里改bk更是无从谈起。后来看前排队伍的wp才意识到这题的标准解法是house of orange。house of orange这套组合拳本质上是三件事劫持top chunk的size让malloc在分配失败时“借刀释放”旧top用unsorted bin attack把一次写操作落到_IO_list_all上最后构造IO_FILE结构在程序退出或报错时通过FSOP链把执行流带到system(/bin/sh)。它是无free菜单题的经典路数也是理解glibc堆管理器“内部动作”和IO_FILE vtable校验机制的绝佳样本。这篇文章就把整条链路从头到尾拆开讲顺便把我踩过的坑也一并写了。1. 无free菜单题的困局与House of Orange的解题思路1.1 常规攻击为什么全线失效在PWN堆题里最顺手的模型永远是“add edit show delete”四件套先打一个fastbin或tcache的double free或者构造unsorted bin attack改掉__malloc_hook或者__free_hook就收工。可一旦题目砍掉delete整套打法瞬间瘫痪。原因很简单glibc的bins里只会出现被释放的chunk。没有free程序自己永远不会把chunk归还给堆管理器你拿着溢出漏洞也只能改写已分配chunk的内容改不了fd/bk因为那些指针根本不存在。就算强行把某个chunk的头部改坏最多也就是让后续malloc报错崩溃谈不上可控利用。所以这类题的第一个思路转折点在于谁的释放不需要程序员手动调用答案是top chunk。当malloc发现top剩余空间不够时会在sysmalloc里做一次特殊的“内部释放”把旧top塞进unsorted bin然后重新划一片堆区当新top。攻击者如果能提前改坏top chunk的size字段就等于凭空制造了一次free。1.2 house of orange的完整链路这个技巧的利用链可以概括成四个阶段劫持top chunk size通过堆溢出把top的size改成一个较小且“看似合法”的值比如0xc1。触发sysmalloc的隐式释放申请一个超大块让malloc走sysmalloc路径旧top被释放进unsorted bin得到libc地址同时伪造fencepost上的新top size为下一轮做准备。unsorted bin attack再次触发释放与分配在unsorted chunk出链时篡改fd/bk把_IO_list_all写成main_arena88。FSOP getShell程序退出或malloc报错时glibc遍历_IO_list_all链表调用vtable里的函数指针最终执行system(/bin/sh)。整个链路没有依赖题目自带的free也没有用到任意地址写原语唯一的输入就是几次add的参数和溢出payload。这就是house of orange最漂亮的地方它让glibc自己的内存管理逻辑变成了攻击者的手。1.3 为什么需要组合拳而不只是unsorted bin attack很多新手会问unsorted bin attack本身只能写一个固定值到任意地址又不能直接控制RIP为什么要和IO_FILE扯上关系因为unsorted bin attack的写入内容固定是main_arena88附近的地址值我们只能控制写到哪里不能控制写入什么。单纯写__malloc_hook为main_arena88没有任何意义写__free_hook也一样程序又没有free调用。所以需要一个“把固定写操作升级为代码执行”的跳板_IO_list_all恰好是这样一个跳板它被写坏后会导致glibc把main_arena里残留的数据当作IO_FILE结构体指针使用而这些数据我们可以在前面的堆操作里“提前布置”成可控的假文件结构。这一环扣一环缺一不可。2. 让sysmalloc“代劳”freetop chunk劫持的底层逻辑2.1 溢出点与top size的物理位置先看堆布局。64位下一个chunk的用户区从chunk地址0x10开始用户数据区长度与size的关系是chunk_size 用户申请大小 0x10且按0x10对齐。top chunk则比较特殊它是arena最末端的大块空闲区size字段就写在它自己的chunk头里。大多数菜单题里add函数如果允许写满甚至写超过申请大小的数据就能从最后一个已分配chunk一路写到top的size字段。也就是说攻击者只需要在最后一次add的content末尾追加8字节这8字节就会覆盖到top的size上。这是整个house of orange的入口漏洞。实际操作时第一步通常先申请一个普通大小的chunk比如add(0x50)然后content写0x50字节占满再加8字节溢出写p64(0xc1)。为什么这样写下一节详细说。2.2 sysmalloc分支什么时候会释放旧top当用户请求大小为nb时malloc首先检查top剩余大小old_size是否够分。判断条件是if (old_size nb MINSIZE) { // 直接切top正常分配 }如果不够进入sysmalloc。sysmalloc里有一整段针对old_top的处理逻辑。核心伪代码大致如下if (old_size MINSIZE) { // 把旧top整块释放进unsorted bin _int_free(av, old_top, 1); // 用old_top的结束位置重新搭一个fencepost当新top av-top (mchunkptr)old_end; }也就是说只要top剩余大小大于最小chunk尺寸64位下MINSIZE是0x20sysmalloc就会先把旧top“丢进回收站”然后才去申请新的内存区。这就是我们要利用的“借刀释放”。释放之后旧top变成unsorted bin里的一个空闲chunk它的fd指针会被系统填成main_arena88unsorted bin的头地址。如果我们有show函数能打印这个旧top的用户区内容就能顺路把libc基地址泄露出来。2.3 修改top size的硬性条件想把top size改成一个能成功触发sysmalloc释放流程的值必须同时满足几个条件缺一个就可能崩条件要求原因最小值size 0x20小于0x20连MINSIZE都不满足直接走其他分支对齐按0x10对齐malloc内部对chunk size有对齐要求不对齐会触发assertPREV_INUSE位一般置1让_int_free认为前一个chunk已分配避免前向合并小于后续请求size nb MINSIZE只有top不够分才会进sysmalloc释放流程经典写法p64(0xc1)就是标准解低三位的标志位里最低位是PREV_INUSE置1表示前块使用中去掉标志后的实际size是0xc0192字节按0x10对齐大于0x20完全满足要求。有些wp写0xd1、0xe1也都可以只是后面布局时要跟着调。2.4 fencepost与新top为什么还要伪造第二轮很多第一次接触这个技巧的人会困惑旧top被释放后新top是谁答案是sysmalloc在旧top原来的位置算出一个fencepost用旧top的size计算出“旧top结束位置”old_end old_top old_size然后把old_end处当作新top的chunk头。这个新top的size字段实际上就是原来old_end那8字节的内容——攻击者是完全可控的。这就是经典exp第二步的由来第一次add(0x1000)触发释放后返回的大块chunk的content可以覆盖到fencepost区域于是顺手在新top的size位置写上0x61让新top只剩0x60大小。这样下一次add(0x60)时又会走一遍sysmalloc释放流程把这个只有0x60的小top也释放进unsorted bin。这个小块size刚好是后续unsorted bin attack需要的尺寸。简单说第二次释放的目的是在unsorted bin里制造一个大小可控的小chunk而不是单纯为了泄露libc。3. Unsorted bin attack为什么盯着_IO_list_all不放3.1 攻击的本质一次固定值的写操作unsorted bin attack的触发条件很经典当malloc从unsorted bin里取一个chunk出来用而这个chunk的bk字段被攻击者改写成了target - 0x10时unlink流程会执行victim-bk-fd unsorted_chunks(av);翻译成人话就是往target这个地址写入main_arena88的值。写入值不受我们控制永远是unsorted bin头地址控制权在“写到哪里”。这个原语看起来非常弱但恰好能用来破坏_IO_list_all。因为_IO_list_all是glibc维护IO_FILE链表头指针的全局变量把它改成一个指向main_arena88的地址等程序走到FSOP流程时glibc会把main_arena88处的数据当成一个_IO_FILE*指针来解引用。3.2 为什么不优先考虑__malloc_hook很多事情能做和值得做是两回事。理论上unsorted bin attack也可以写__malloc_hook触发点在下次malloc但实际利用价值很低写入值是main_arena88这个值不是one_gadget地址。想让main_arena88附近恰好是一个one_gadget或system地址需要极其苛刻的堆布局。后续malloc如果正好从那个地址分配chunk并写数据倒是能二次改写成one_gadget但house of orange的场景里请求的size往往由题目功能固定不好凑。_IO_list_all就不一样。它被写坏后glibc内部会遍历链表把链表上的“假FILE结构”当作真对象来调用只要布局凑好稳定可控不需要任何二次写原语。3.3 为什么需要0x60大小的小chunkhouse of orange经典exp里目标尺寸是0x60。原因藏在IO_FILE遍历的细节里。_IO_list_all被写成main_arena88后FSOP流程会把main_arena88当作fp也就是一个_IO_FILE*。这个地址附近的字节会被解释成IO_FILE的各个字段_IO_write_base、_IO_write_ptr、_chain、vtable全是main_arena里的数值。攻击者没法直接改main_arena但可以通过small bin的写入间接控制它当我们把一个0x60大小的chunk释放进small bin时glibc会把small bin链表的fd/bk写到对应的bin数组位置而这个位置经过偏移计算正好落在main_arena88附近。于是main_arena里的某些“字段”就被替换成了我们可控堆块的地址。这相当于用堆管理器自己的链表维护动作替我们把假FILE结构体的关键指针安插到了正确位置。这也是house of orange最精巧的部分不只是伪造IO_FILE而是把伪造工作拆成了几个堆管理器自动完成的步骤。4. FSOP落地把IO_FILE伪造到能直接getshell4.1 _IO_list_all遍历与触发时机FSOP的全称是File Stream Oriented Programming核心入口是_IO_list_all这个全局链表。glibc在程序退出exit、异常终止abort或部分malloc报错路径里会调用_IO_flush_all_lockp遍历_IO_list_all上每个FILE结构体对带缓冲的文件执行flush。flush过程中如果发现某个FILE的_IO_write_ptr _IO_write_base就会调用它的__overflow方法也就是vtable偏移0x18处的函数指针。一旦我们能控制这个vtable就相当于控制了一次间接调用。在house of orange的经典链路里_IO_list_all已经被写坏第二次add或程序退出时就会触发这条路径此时fp指向的是main_arena88附近的伪造结构。4.2 vtable替换与_IO_str_jumps64位下_IO_FILE结构体的末尾有一个vtable指针指向一组IO函数指针表。如果vtable被替换成_IO_str_jumps的地址那么__overflow对应的就是_IO_str_overflow函数。_IO_str_overflow是glibc用于“字符串流”扩容的函数。它的大致逻辑是如果当前缓冲区不够就调用fp-_s._allocate_buffer(new_size)来重新分配内存。问题在于_s._allocate_buffer这个字段在文件结构体里是一个函数指针——如果我们把假FILE里该字段改成system的地址再把另一个字段改成/bin/sh的地址那么_IO_str_overflow就会执行system(/bin/sh)。vtable校验这块传统house of orange基于glibc 2.23当时还没有IO_validate_vtable到了2.24以后glibc加了vtable地址段检查要求fp-vtable必须落在__libc_IO_vtables段内_IO_str_jumps本身就在这个段里所以2.24依然可以打。2.26以后tcache出现并且vtable检查更严链路就需要调整。4.3 假FILE结构的关键字段布局一个可用的伪造IO_FILE结构体关键位置要填这些内容偏移64位字段写入值0x00 ~ 0x17chunk头/占位按堆块布局填0x18_IO_write_base指向/bin/sh字符串的地址0x20_IO_write_ptr大于_IO_write_base的值触发flush0x28其他数据维持合法布局避免校验崩溃0xd8附近_chain指向下一个假FILE或NULLvtable偏移vtable指针_IO_str_jumps的地址house of orange的具体利用里这些“字段”常常不是直接写的而是main_arena88处本来就有的一些值经过small bin插入后恰好变得可用。所以实战中不建议死记偏移而是用gdb把触发FSOP时fp实际指向的内存dump下来再对着_IO_FILE结构体定义逐个核对。4.4 libc版本差异一览libc版本vtable校验tcachehouse of orange可用性2.23及以前无无经典打法稳定2.24IO_validate_vtable无_IO_str_jumps仍在vtables段总体可用2.26检查加强有链路需调整经典打法失效这也是为什么所有house of orange的wp都会强调“先查libc版本”。同一个exp在2.23的libc上弹shell换到2.24的libc可能就因为vtable校验直接崩了。5. 一段通用exp骨架从泄露地址到弹出shell5.1 题目功能抽象与泄露思路为了方便说清payload我把题目抽象成最简单的三个函数add(size, content)申请并写入show(idx)打印某个已分配chunk的内容均无delete。漏洞点是add的content长度可以超过size导致溢出。泄露libc的手段很自然第一次add(0x1000)触发sysmalloc后旧top已经进入unsorted binfd被写成main_arena88。旧top原来是哪个chunk它虽然被释放了但它的前一个chunk我们第一次add的0x60小chunk仍然是已分配状态show它时读取的是它自己的内容读不到旧top的fd。所以需要想个办法让show能够打印到unsorted bin chunk的fd。常见做法是第一次add的chunk不要和top相邻或者在第二次add之后show一个能通过偏移落到旧top区域的chunk索引。这属于题目细节但有个相对通用的技巧第一次溢出后第二次add返回的大chunk内容其实就出现在旧top之后如果程序对已分配chunk的内容不做长度限制show这个新chunk即可读到unsorted bin的fd。示例exp骨架如下from pwn import * context.arch amd64 def add(size, content): ru(b ) sendlineafter(badd, b1) # 按题目菜单调整 sendlineafter(bsize, str(size)) sa(bcontent, content) def show(idx): sendlineafter(b , b2) sendlineafter(bindex, str(idx)) return ru(b\n) # step1: 改坏top chunk size add(0x50, bA*0x50 p64(0xc1)) # step2: 触发sysmalloc释放旧top进unsorted bin伪造fencepost新top add(0x1000, fit({ 0x10: p64(0) p64(0x21), # 预置一个小chunk头给后续布局用 0x30: p64(0xf1), # 控制fencepost的size 0x50: p64(0x61), # 再伪造下一轮的top size })) # show某个能打到unsorted bin的索引拿libc基址 leak show(...) libc.address u64(leak) - libc.sym[main_arena] - 88 # step3: 布局unsorted bin attack 假IO_FILE payload fit({ # 在0x60的unsorted chunk上布置假IO关键偏移按libc计算 0x...: libc.sym[_IO_list_all] - 0x10, # 作为bk 0x...: p64(libc.sym[_IO_str_jumps]), 0x...: p64(libc.sym[system]), 0x...: binsh_addr, }) add(0x60, payload) # 触发FSOP ru(b ) sendline(b3) # exit p.interactive()5.2 三个add的payload逻辑拆解第一个add(0x50, bA*0x50 p64(0xc1))是入口。0x50是用户区大小实际chunk 0x60从用户区末再往后8字节正好是top的size字段。第二个add(0x1000)是整条攻击链的发动机。它同时完成了三件事触发旧top释放进unsorted bin泄露libc在返回的大chunk内容里覆盖fencepost伪造新top size为0x61提前埋下一部分后续需要的chunk头。fit里的偏移需要根据题目给的堆布局用gdb现场算不同题目差别很大。第三个add(0x60)是扣扳机。0x60的请求会让malloc把unsorted bin里那个0x60的小chunk取出来出链时执行unsorted bin attack把_IO_list_all写成main_arena88。同时这个0x60的chunk自身的数据区又被预置成假IO_FILE的各个字段等FSOP遍历时就是现成的弹药。5.3 调试时的观察点跑这个exp时建议在第三个add前后各下一次断点观察三处关键内存gdb-peda$ x/gx _IO_list_all gdb-peda$ x/20gx main_arena88 gdb-peda$ x/30gx 0x... # 伪造的IO_FILE所在堆块重点确认_IO_list_all是否变成了main_arena88附近的地址main_arena88处是否出现了一个可控堆块的指针假FILE结构的vtable是否指向_IO_str_jumps。任何一步不对都不要急着往下打先对比偏移。6. 翻车实录那些让exp失效的隐藏细节6.1 tcache的干扰与libc版本陷阱如果题目的libc是2.26及以上house of orange的经典链路会多出一个变数tcache。0x60大小的chunk释放后会先进tcache而不是unsorted bin这直接破坏了“小top释放进unsorted bin再被取出触发attack”的前提。所以拿到题先别急着写expchecksec看不了libc版本用pwninit或ldd确认一下。要是libc是2.27又想在无free题目里用unsorted bin attack就得考虑先填满tcache或者改用large bin attack的思路那又是另一套打法了。6.2 movaps栈对齐system就差这一下这是所有FSOP利用里最隐蔽的坑。64位glibc对很多函数调用加了SSE对齐要求当_IO_str_overflow最终调到system时如果rsp不满足16字节对齐system内部执行movaps指令会直接SIGSEGV。现象就是exp跑起来没有任何报错提示程序直接段错误gdb里看到崩溃在libc的memcpy或system内部。解决办法是在假FILE里做函数指针链时加一个ret gadget。比如把_allocate_buffer位置不直接填system而是填libc.address ret_gadget让栈指针先弹齐接着才真正进入system。每条链的栈偏移不一样只能靠gdb调。6.3 偏移计算与gdb调试心得布局偏移是整个house of orange里最容易错的地方。我自己踩过的坑包括_IO_list_all - 0x10少算、main_arena88误写成main_arena、0x60的chunk头占位导致数据区起点差8字节。经验是永远不要凭记忆写偏移用gdb算。在第二个add之后用x/gx查看返回chunk的地址和fencepost地址把两者的差记下来再回到exp里把fit的偏移替换成实测值。fp的vtable指向哪、_chain链到哪也全部以内存dump为准。6.4 没有show时的替代方案严格来说house of orange最标准的形态依赖show来泄露libc。如果题目连show都没有也不是完全没法打但难度陡增。常见方向是partial overwriteunsorted bin的fd低字节被写成本地已知的偏移盲打远程时爆破1~2字节或者通过报错信息把地址带出来。这些方案都有运气成分不推荐作为第一选择。拿到无free无show的题优先考虑能不能改stdout的IO结构来泄露或者直接上one_gadget做多阶段攻击反而更稳。最后讲一个我自己的调试习惯每到一个阶段就先把当前堆布局完整打印出来对比一遍预期。house of orange的每一环都高度依赖堆管理器“自动帮你做某事”一旦中间哪一步的size、fencepost、small bin指针没对齐后面全盘崩。所以宁可多花十分钟在gdb里确认fencepost的size也别直接拿完整的exploit去远程赌。这个习惯救了我好几次也推荐给所有第一次接触这套技巧的朋友。
RELATED

相关推荐

基于WebSocket的实时对战游戏Demo:通信协议、状态同步与避坑实践

基于WebSocket的实时对战游戏Demo:通信协议、状态同步与避坑实践

简介:基于WebSocket的在线游戏开发Demo是一份面向游戏前端或Node.js/Go开发者的实战示例包。资源围绕WebSocket实时双向通信机制,展示了从HTTP升级握手、数据帧解析到客户端/服务器双向消息推送的完整链路,并给出玩家操作同步与服务器状态广播…

📅 2026/10/12 8:17:49
从战略解码到结果应用:企业绩效管理全流程拆解与落地指南

从战略解码到结果应用:企业绩效管理全流程拆解与落地指南

前阵子在一批管理咨询材料里翻到一套编号为DG1067的企业绩效管理PPT资料,整整104页,信息密度相当高。这套资料不是市面上那种只讲KPI和打分表的基础教程,而是把战略解码、目标拆解、过程管理、绩效评价、结果应用、组织与个人联动这一整条链路…

📅 2026/10/12 8:17:49
Claude Fable 5 语音情感识别接入:根据用户音调动态渲染网页情绪粒子

Claude Fable 5 语音情感识别接入:根据用户音调动态渲染网页情绪粒子

多模态人机交互如果仅仅停留在“把文本打在屏幕上”,界面终究是冰冷的。在音乐现场,贝斯的低频震动会直接通过地板传到观众胸腔,主唱音色从低沉呢喃转为撕裂高亢时,灯光师的推子会同步切到冷蓝或爆裂暖红。这种声光协同的生理级反…

📅 2026/10/12 8:12:49
MORE NEWS

更多资讯

📰

RT-Thread线程切换汇编深度解析:PendSV与上下文切换机制

1. 为什么值得死磕线程切换里的那几行汇编很多人学嵌入式实时操作系统,学着学着就卡在一个地方:C 语言部分全能看懂,调度器逻辑也能画出来,但一旦翻到context_switch或者PendSV_Handler那几行汇编,整个人就懵了。寄存器…

📰

AutoJs 4.1.0 自动化脚本实战:架构设计、控件定位与稳定性优化

1. 为什么我还在用 AutoJs 4.1.0 做自动化聊 AutoJs 之前,先说一个我自己的真实感受:这个工具在圈子里属于那种“用的人一直在用,不用的人永远看不上”的类型。我接触 AutoJs 大概是在它 3.x 版本的时候,后来 4.1.0 出来之后&…

📰

Postman接口自动化测试:从变量断言到Newman可视化报告全流程实践

1. 先别急着点“Send”:想清楚Postman做自动化到底要解决什么问题很多朋友第一次听说“用Postman做接口自动化测试”时,第一反应是:Postman不就是个手动调接口的工具吗?在地址栏里输URL、点Send、看返回体,顶多把请求存…

📰

代码评审从流程到落地:五年实践总结的 Code Review 指南

代码评审这事儿,我在团队里推行了差不多五年,从最开始大家觉得是“浪费时间”到后来变成“没有评审不敢合代码”,中间踩过的坑比我写过的 Bug 还多。今天就把这套相对完整的代码评审实践“open-code-review”拆开聊聊,从流程设计、…

📰

Android通用Hook框架设计与实现:端侧注入与规则下发

做Android开发的同学,看到“基于PHP、asp.net、java、Springboot、SSM、vue3的基于Android平台的通用Hook框架设计与实现”这个标题,第一反应多半是“这到底是个啥组合”。说实话我第一次拿到这个项目清单的时候也愣了一下:Hook框架是跑在And…

📰

FLAC隧道支护模拟实战:锚杆锚索喷混钢拱架参数与FISH代码解析

地铁隧道初支方案里的“锚杆锚索喷浆钢拱架”四件套,算不算最优解?说实话,在FLAC里把这四类支护结构建出来、调通、算跑,很多工程师能做得七七八八,但真正能把每一条FISH代码背后的力学逻辑讲清楚的人并不多。我前前后…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬