【学习记录】Week13(三):House of Orange 经典复现与 exit 机制暗线劫持
写在前面在上一篇中我们深入探讨了_IO_FILE的任意地址读写技巧与 vtable 校验的演进史。今天我们将把堆漏洞与 IO_FILE 结合起来复盘 glibc 2.23 时代的绝对经典——House of Orange。更重要的是我们将把目光从_IO_list_all转向程序退出的另一条必经之路exit机制探索__exit_funcs与tls_dtor_list这两条独立于 FSOP 的高阶攻击面。 目录经典重温House of Orange 与 Unsorted Bin Attack 的合谋独立攻击面exit机制与__exit_funcs劫持线程局部存储暗线tls_dtor_list劫持时代的眼泪__malloc_hook/__free_hook的最后辉煌 (≤2.33)总结与下篇预告1. 经典重温House of Orange 与 Unsorted Bin Attack 的合谋在 Week11 中我们提过House of Orange 解决了“无free函数如何释放堆块”的问题通过修改 Top Chunk size 触发sysmalloc将旧 Top 放入 Unsorted Bin。但它更精髓的部分在于如何利用这个 Unsorted Bin chunk 完成 FSOP。1.1 2.23 时代的无脑 FSOP在 glibc 2.23 中没有 vtable 校验。House of Orange 的利用链如下获取 Unsorted Bin chunk通过 Top Chunk 劫持旧 Top Chunk 进入 Unsorted Bin。Unsorted Bin Attack利用漏洞修改该 chunk 的bk指针为_IO_list_all - 0x10。当下次malloc遍历 Unsorted Bin 时触发bck victim-bk; unsorted_chunks(av)-bk bck; bck-fd unsorted_chunks(av);这会将main_arena的地址写入_IO_list_all。链表错位与伪造此时_IO_list_all指向了main_arena的头部。glibc 遍历_IO_list_all时会把main_arena当作一个_IO_FILE结构体。由于_chain字段偏移 0x68恰好落在main_arena的 Smallbin 链表中攻击者可以通过精心计算大小让 Smallbin 的fd指针指向我们在堆上伪造的 Fake FILE。触发与执行当程序触发malloc_printerr或调用exit时glibc 遍历到 Fake FILE发现_IO_write_ptr _IO_write_base调用vtable-overflow。由于无 vtable 校验直接执行system(/bin/sh)。1.2 2.24 的失效随着 glibc 2.24 引入IO_validate_vtable上述第 4 步中直接伪造vtable指向堆表的行为会被拦截。这催生了后续利用合法虚表_IO_str_jumps,_IO_wfile_jumps的绕过技术如 House of Pig / Apple。2. 独立攻击面exit机制与__exit_funcs劫持除了 FSOP程序退出时还有另一套执行逻辑。exit()函数在刷新 IO 流之后会调用注册的析构函数。这就是__exit_funcs攻击面。2.1exit内部调用链void exit(int status) { __run_exit_handlers(status, __exit_funcs, true, true); }__exit_funcs是一个指向exit_function_list结构体链表的全局指针。在__run_exit_handlers中会遍历这个链表并根据每个节点的flavor类型如ef_cxa,ef_on,ef_at调用对应的函数指针。2.2 劫持思路如果通过堆漏洞如任意地址写能够覆盖 libc 中全局的__exit_funcs指针使其指向我们在堆上伪造的exit_function_list结构体程序exit时就会执行我们构造的函数指针。2.3 致命阻碍PTR_MANGLE (指针加密)在 glibc 2.24 中为了防止这种劫持__run_exit_handlers在调用函数指针前会使用PTR_DEMANGLE宏对指针进行解密// 加密逻辑函数指针与线程控制寄存器 (通常存放于 fs/gs 段的某个偏移如 fs:0x30) 异或后再旋转 encrypted_ptr (func_ptr ^ secret) 0x11; // 解密逻辑 decrypted_ptr (encrypted_ptr 0x11) ^ secret;绕过策略如果我们要伪造加密的函数指针必须知道secret通常称为 Pointer Guard。在题目存在严重信息泄露如能泄露 TLS 基址和fs:0x30的值时可以计算出secret从而伪造加密后的system地址。但这在实战中条件极为苛刻。3. 线程局部存储暗线tls_dtor_list劫持既然__exit_funcs的指针被加密了安全研究员又发现了另一条路tls_dtor_list。3.1 TLS 析构机制在多线程环境下线程退出时会调用__call_tls_dtors来清理线程局部存储相关的资源。void __call_tls_dtors(void) { while (tls_dtor_list) { struct dtor_list *cur tls_dtor_list; // 调用析构函数 cur-func(cur-obj); tls_dtor_list tls_dtor_list-next; } }tls_dtor_list是一个存在于线程控制块TCB通常由fs或gs寄存器寻址中的指针。3.2 致命缺陷与利用令人兴奋的是在 glibc 2.34 之前的某些版本中__call_tls_dtors调用cur-func时并没有使用 PTR_DEMANGLE 进行解密这意味着只要我们能通过堆漏洞实现任意地址写将tls_dtor_list指向我们在堆上伪造的dtor_list结构体并在其中写上system的地址和/bin/sh的参数程序退出时就会直接调用system(/bin/sh)完全绕过了 vtable 检查和__exit_funcs的加密如何写tls_dtor_listtls_dtor_list存放在 TLS 中TLS 的地址通常在 libc 之前由mmap分配。如果题目允许大规模堆溢出或存在任意地址写可以通过覆盖 libc 附近的内存来篡改这个指针。这也是现代高版本 glibc 利用的一条重要暗线。4. 时代的眼泪__malloc_hook/__free_hook的最后辉煌 (≤2.33)在谈论了这么多复杂的绕过我们不能忘记过去十年 PWN 题的“标准答案”。4.1 一击必杀的 Hook在 glibc 2.33 及以前版本libc 数据段中存在两个裸的函数指针__malloc_hook__free_hook每当调用malloc或free时glibc 会首先检查这两个指针是否为空如果不为空则跳转执行。利用流程极其简单通过堆漏洞泄露 Libc 基址。通过 Tcache Poisoning 或 Fastbin Attack 实现“任意地址写”。将__free_hook覆盖为system地址。释放一个内容为/bin/sh\x00的 chunk。free(/bin/sh)- 触发__free_hook- 执行system(/bin/sh)。4.2 时代的终结由于这种利用方式过于无脑使得堆漏洞利用变成了“找 Hook”的流水线作业。glibc 官方在 2.34 版本中以“安全性能低于预期”为由彻底移除了这两个 Hook 变量。这标志着“裸 Hook 时代”的终结逼迫所有 CTF 选手转向了以_IO_FILE(FSOP) 和exit机制为代表的“结构体伪造时代”。5. 总结与下篇预告5.1 核心知识点总结House of Orange经典展示了如何将堆块状态破坏与 Unsorted Bin Attack 结合进而劫持_IO_list_all完成链表劫持。exit攻击面__exit_funcs虽然是退出必经之路但受制于PTR_MANGLE指针加密tls_dtor_list提供了无加密的替代路径但需要定位 TLS 地址。Hook 的兴衰__malloc_hook/__free_hook是低版本的利器但在高版本中已成历史理解其原理有助于体会现代利用复杂度的提升。5.2 下篇预告在 Week13 的最后我们将进行stderr/stdout独立攻击面与综合防御机制总结。深入探讨如何在不修改_IO_list_all的情况下仅通过篡改stderr或stdout完成信息泄露与控制流劫持。梳理 glibc 在各版本中对 IO_FILE 防御的补丁历史。Week 13 全局总结。结语从 House of Orange 的链表错位到exit机制的暗度陈仓再到 Hook 的简单粗暴。漏洞利用的历史就是一部攻防对抗的演化史。高版本的复杂利用往往是对低版本简单逻辑的层层包装与绕过。理解这些历史的演进能让你在面对未知的 glibc 版本时拥有更敏锐的嗅觉。