写在前面:在上一篇中,我们掌握了 House of Botcake,成功在现代 glibc (2.29+) 中制造了堆重叠并实现了 Tcache Poisoning。然而,在 glibc 2.34 中,官方彻底移除了 __free_hook 和 __malloc_hook。这就意味着,即便我们通过 Tcache Poisoning 拿到了任意地址写的原语,也面临着“写到哪、写什么才能劫持控制流”的困境。今天,我们将学习现代 CTF 中的另一大杀器——House of Pig。它将 Tcache 的分配能力与 IO_FILE (FSOP) 完美融合,在无 Hook 的时代为我们撕开通往 Shell 的突破口。

📑 目录

  1. 时代背景:Hook 没落与 FSOP 的崛起
  2. 核心跳板:_IO_str_overflow 源码剖析
  3. House of Pig 利用流程与数学推导
  4. 实战进阶:与 House of Botcake 的经典组合拳
  5. 总结与下篇预告

1. 时代背景:Hook 没落与 FSOP 的崛起

在 glibc 2.34 之前,堆漏洞利用的终点往往是覆盖 __free_hook 为 system,然后 free("/bin/sh")。随着 Hook 被移除,攻击者必须寻找新的“控制流劫持点”。

此时,FSOP (File Stream Oriented Programming) 成为了主流。FSOP 的核心是伪造 IO_FILE_plus 结构体,并通过某种方式触发 glibc 的 IO 流刷新机制(如调用 exit() 或触发 malloc_printerr)。在刷新过程中,glibc 会遍历 _IO_list_all 链表,调用每个 FILE 结构体虚表中的 overflow 函数。

然而,glibc 2.24 引入了 vtable 地址范围检查,我们不能再随意伪造一个虚表指针指向栈上的 shellcode。我们必须寻找合法 vtable 中的危险函数。House of Pig 利用的正是 _IO_str_jumps 这个合法虚表中的 _IO_str_overflow 函数。

2. 核心跳板:_IO_str_overflow 源码剖析

_IO_str_overflow 是 glibc 中处理字符串流缓冲区溢出的函数。当写入的数据超过当前缓冲区大小时,它会尝试分配一个更大的缓冲区。其核心 C 代码逻辑(简化后)如下:

int _IO_str_overflow (_IO_FILE *fp, int c) {
    int flush_only = c == EOF;
    size_t pos;
    if (fp->_flags & _IO_NO_WRITES) // 必须可写
        return flush_only ? 0 : EOF;
    
    // 计算当前写入位置
    pos = fp->_IO_write_ptr - fp->_IO_write_base;
    
    if (pos >= (size_t) (_IO_blen (fp) + flush_only)) {
        if (fp->_flags & _IO_USER_BUF) /* not allowed to enlarge */ 
            return EOF;
        else {
            // 核心逻辑:分配新缓冲区
            char *new_buf;
            char *old_buf = fp->_IO_buf_base; // 旧缓冲区
            size_t old_blen = _IO_blen (fp);  // 旧缓冲区长度
            // 计算新长度,通常是 old_blen * 2 + 100
            size_t new_size = 2 * old_blen + 100;
            
            // 【漏洞点 1】调用 malloc,大小受我们控制!
            new_buf = malloc (new_size); 
            if (new_buf == NULL) {
                // 分配失败的处理...
            }
            
            // 复制旧数据到新缓冲区
            memcpy (new_buf, old_buf, old_blen);
            
            // 【漏洞点 2】调用 free,参数受我们控制!
            if (old_buf != NULL)
                free (old_buf); 
                
            // 更新 FILE 结构体的指针...
        }
    }
    // ...
}

关键发现
在这个函数中,malloc 的参数 new_size 由 _IO_buf_end - _IO_buf_base 计算得出,而 free 的参数 old_buf 就是 _IO_buf_base
如果我们能伪造一个 IO_FILE_plus 结构体,并让其 vtable 指向 _IO_str_jumps,我们就能控制 malloc 的大小和 free 的参数

3. House of Pig 利用流程与数学推导

House of Pig 的核心思想是:利用 Tcache Poisoning 劫持 malloc 的分配流,使得 _IO_str_overflow 中的 malloc(new_size) 返回我们想要的目标地址(如栈地址或 _IO_list_all 本身),然后再利用 free(old_buf) 触发执行。

3.1 构造伪造的 FILE 结构体

为了顺利走到 malloc 和 free,我们需要精心设置 IO_FILE_plus 的字段:

  1. _flags: 设置为 0 (可写,且绕过 _IO_USER_BUF 检查)。
  2. _IO_write_base: 设置为 0
  3. _IO_write_ptr: 设置为 1 (保证 pos >= _IO_blen(fp) 触发扩容逻辑)。
  4. _IO_buf_base: 设置为 "/bin/sh\x00" 的地址 (作为 free 的参数,如果是覆盖 __free_hook 时代的变体则用此法;在无 Hook 时代,通常设为旧缓冲区地址,配合栈迁移)。
  5. _IO_buf_end: 设置为 0。这样 old_blen = 0 - _IO_buf_basenew_size = 2 * (-_IO_buf_base) + 100
  6. vtable: 指向 _IO_str_jumps

3.2 结合 Tcache 的劫持逻辑

假设我们已经通过 House of Botcake 拿到了 Tcache Poisoning 能力,并且计算出了 new_size 落在某个 Tcache bin 的范围内(例如 0x60)。

  1. 我们在堆上伪造上述的 IO_FILE_plus 结构体。
  2. 利用 Tcache Poisoning,将 Tcache[0x60] 的 next 指针指向目标地址(例如存放返回地址的栈地址)。
  3. 利用 Largebin Attack 或 Tcache Poisoning 将 _IO_list_all 覆盖为我们伪造的堆块地址。
  4. 调用 exit(0) 触发 _IO_flush_all_lockp
  5. glibc 遍历到我们伪造的 FILE 结构体,调用 _IO_str_overflow
  6. 函数内部执行 malloc(new_size)。因为 Tcache[0x60] 被投毒,这次 malloc 直接返回了栈地址
  7. 接着执行 memcpy(new_buf, old_buf, old_blen)。我们将精心构造的 ROP 链作为 old_buf,长度作为 old_blen,直接将 ROP 链写入了栈上!
  8. 随后函数返回,栈展开,直接执行我们的 ROP 链(如 system("/bin/sh") 或 setcontext)。

准备: House of Botcake 获取 Tcache 投毒能力

伪造 IO_FILE_plus 结构体
vtable=_IO_str_jumps

计算 new_size 并投毒对应 Tcache bin
使 next 指向栈地址

劫持 _IO_list_all 指向伪造结构体

触发 exit 触发 FSOP

调用 _IO_str_overflow

malloc new_size
从 Tcache 取回栈地址

memcpy 将 ROP 链写入栈地址

函数返回执行 ROP 链

Getshell

4. 实战进阶:与 House of Botcake 的经典组合拳

在真实的 glibc 2.31~2.35 CTF 题目中,House of Botcake 和 House of Pig 往往是相伴而生的。一个完整的利用链如下:

  1. 漏洞触发:利用 UAF 或 Off-by-one。
  2. 堆重叠制造:使用 House of Botcake,让 chunk A 同时在 Unsorted Bin 和 Tcache 中。
  3. 信息泄露:通过 Unsorted Bin 中的 A 泄露 Libc 基址;通过 Tcache 中的 A 泄露堆地址(用于绕过 Safe-Linking)。
  4. 原语获取:修改 A 的 Tcache next 指针,实现任意地址分配(Tcache Poisoning)。
  5. 结构体伪造:利用任意地址分配,在堆上布置伪造的 IO_FILE_plus 结构体。
  6. 劫持与触发 (House of Pig):再次利用 Tcache Poisoning 修改 _IO_list_all,或利用 Unsorted Bin Attack 修改 _IO_list_all。调用 exit 触发 FSOP。
  7. 控制流劫持_IO_str_overflow 内部的 malloc 取回栈地址,memcpy 写入 ROP 链,完成栈迁移或直接执行 system

这套组合拳完美规避了 Double Free 检查、Safe-Linking 以及 Hook 移除带来的障碍,是现代堆利用的“标准答案”。

5. 总结与下篇预告

5.1 核心知识点总结

  1. FSOP 是现代替代方案:在 __free_hook 被移除后,伪造 IO_FILE 结构体并触发 overflow 是劫持控制流的核心路径。
  2. _IO_str_overflow 是跳板:这个合法虚表函数内部包含 malloc 和 free 调用,其参数受 FILE 结构体字段控制。
  3. Tcache 与 FSOP 的结合:House of Pig 的精髓在于,利用 Tcache Poisoning 控制 _IO_str_overflow 中 malloc 的返回地址,将数据(如 ROP 链)精准写入目标地址(如栈帧),实现“任意地址写任意值”并触发执行。

5.2 下篇预告

在掌握了 Botcake 和 Pig 这两大现代基石后,下一篇我们将转向另一个高频考点:House of Corrosion(global_max_fast 利用)
我们将探讨如何通过破坏 global_max_fast 这个全局变量,将原本只能管理小块内存的 Fastbin 机制扩展到全尺寸,从而实现对更大范围内存(如 stdout 或 _IO_list_all)的强力劫持。

结语:House of Pig 展示了现代漏洞利用的典型范式——不再追求单点突破,而是将多种机制(Tcache 的分配能力 + IO_FILE 的执行路径)串联起来,形成一条从内存破坏到代码执行的完整通道。掌握这种“组合思维”,是进阶高阶 PWN 选手的关键。

Logo

欢迎加入 MCP 技术社区!与志同道合者携手前行,一同解锁 MCP 技术的无限可能!

更多推荐