【学习记录】Week13(二):伪造 FILE 结构体任意读写与 vtable 校验绕过演进史

发布时间:2026/7/4 16:57:24
【学习记录】Week13(二):伪造 FILE 结构体任意读写与 vtable 校验绕过演进史 写在前面在上一篇中我们从宏观角度精读了_IO_FILE的结构布局与fopen/fread/fwrite/fclose的内部调用链理解了 FSOP 的触发原理。今天我们将视角拉近深入到实战中最实用的两个微观领域如何利用_IO_buf_base和_IO_buf_end实现任意地址读写以及glibc vtable 校验机制的演进史与绕过思路。这是将理论转化为武器的关键一步。 目录任意地址读写控制_IO_buf_base/_IO_buf_end的魔法独立攻击面劫持stdout泄露信息vtable 校验机制演进史结合 glibc 版本表经典绕过思路从伪造任意虚表到滥用合法虚表总结与下篇预告1. 任意地址读写控制_IO_buf_base/_IO_buf_end的魔法在 glibc 的 IO 缓冲区管理中_IO_buf_base和_IO_buf_end划定了底层物理缓冲区的起止边界。所有通过fread读入的数据都会先被系统调用放进这个区间所有通过fwrite写出的数据如果触发了刷新也是从这个区间提取。如果存在 UAF 或堆溢出能够修改某个 FILE 结构体如stdin或stdout的这两个指针我们就能将缓冲区“移花接木”到任意地址从而实现极其强大的任意读写原语。1.1 任意地址读 (利用 fread / scanf / gets)假设我们劫持了stdin_IO_2_1_stdin_希望读取target_addr处的数据。修改stdin的字段_IO_buf_basetarget_addr_IO_buf_endtarget_addr read_size_IO_read_ptr_IO_read_end_IO_buf_end制造缓冲区已空的假象迫使 glibc 执行__underflow重新填充缓冲区。当程序调用fread、scanf或gets时触发__underflow。glibc 发现缓冲区空了于是调用__read(系统调用read) 从键盘读取输入。关键点glibc 会将读取的数据写入_IO_buf_base到_IO_buf_end之间由于我们修改了这两个指针我们输入的数据会直接写入target_addr。*等等这不是任意写吗怎么是任意读*任意读进阶如果我们劫持了stdout将其_IO_buf_base设为target_addr然后调用fwrite或printf。glibc 会把target_addr的数据当作缓冲区数据通过write系统调用打印到屏幕上从而实现任意地址读1.2 任意地址写 (利用 fwrite / printf)如果我们能控制输出流stdout的缓冲区指针就能向任意地址写入数据。修改stdout的字段_IO_buf_basetarget_addr_IO_buf_endtarget_addr write_size_IO_write_basetarget_addr_IO_write_ptrtarget_addr data_len我们预先把要写入的 payload 布局好或者利用程序的某些固定输出。当触发fwrite时glibc 发现_IO_write_ptr _IO_write_base触发__overflow或__write将target_addr到target_addr data_len的数据输出。但更常见的是结合fread实现任意写如 1.1 中所述。实战意义在难以构造完整 FSOP 链如 vtable 难以绕过的低版本 glibc 题目中直接修改stdout的_IO_buf_base进行任意地址读写往往是最先要获取的原语。2. 独立攻击面劫持stdout泄露信息在 CTF 中stdout_IO_2_1_stdout_是一个极其高频的攻击目标。它位于 libc 数据段且通常可以通过 Unsorted Bin Attack 或 Tcache Poisoning 被部分覆盖。经典利用场景部分覆盖_IO_write_base在 glibc 2.23~2.27 中stdout的_flags通常为0xfbad2887。其_IO_write_base的末尾几位通常为\x00。如果存在一个 Off-by-one 或单字节溢出将_IO_write_base的最低位覆盖为\x00甚至覆盖更前面的字节会使得_IO_write_base向前扩展包含到 libc 中的某些敏感数据如_IO_2_1_stdin_的地址或 environ 指针。当程序下次调用puts或printf时由于_IO_write_ptr _IO_write_baseglibc 会把这块多出来的内存数据打印出来从而无脑泄露 Libc 基址。3. vtable 校验机制演进史结合 glibc 版本表随着攻击者频繁伪造vtable指针指向栈或堆上的伪造函数表glibc 官方开始出手干预。理解这个演进史是掌握现代 FSOP 技术的前提。glibc 版本vtable 校验机制演进攻击者的应对策略2.23 及以下无校验。vtable可以指向任何地址。直接伪造vtable指向堆上的假表将overflow偏移处写为system实现盲打 GetshellHouse of Orange 原版。2.24引入IO_validate_vtable。调用虚函数前检查vtable是否落在合法的__libc_IO_vtables段内。越界则调用IO_vtable_check触发 abort。无法伪造任意 vtable。转而寻找合法 vtable 内部的危险函数如_IO_str_jumps中的_IO_str_overflow内含malloc/free调用。2.28 - 2.31封堵了部分_IO_str_overflow中的逻辑增加了对_IO_str_jumps调用的条件检查。转向利用_IO_wfile_jumps宽字符虚表利用其复杂的调用链跳板。2.34彻底移除__malloc_hook和__free_hook。FSOP 成为唯一的主流控制流劫持方式。_IO_wfile_jumps的利用House of Apple 系列走向成熟。2.35引入更多对_IO_FILE_plus内部指针的一致性检查但由于_wide_data-_wide_vtable的设计架构未变依然无法彻底封堵。继续利用 House of Apple 2/3结合exit中的tls_dtor_list等其他攻击面。4. 经典绕过思路从伪造任意虚表到滥用合法虚表4.1 2.24 之前的无脑绕过已失效但需理解原理假设我们要调用system(/bin/sh)。在堆上布置/bin/sh\x00。紧接着布置一个伪造的vtable在vtable 0x18__overflow的偏移处写入system的地址。伪造_IO_FILE_plus设置_IO_write_ptr _IO_write_basevtable指向堆上的假表。触发 FSOPglibc 调用vtable-__overflow(fp, EOF)。实际执行system(fp)。只要fp开头是/bin/sh即可 Getshell。4.2 2.24 的合法虚表滥用现代基石由于vtable必须在__libc_IO_vtables段内我们只能使用 glibc 自带的虚表如_IO_file_jumps(标准文件操作)_IO_str_jumps(字符串流操作)_IO_wfile_jumps(宽字符文件操作)House of Apple 的核心破局点虽然外层vtable被限制在合法区间内但 glibc 的检查到此为止当我们使用合法的_IO_wfile_jumps时其内部的_IO_wfile_overflow函数会解引用 FILE 结构体中的_wide_data指针并调用_wide_data-_wide_vtable中的虚函数。致命缺陷glibc没有对_wide_data-_wide_vtable进行范围检查这意味着我们可以让外层vtable老老实实指向_IO_wfile_jumps骗过安检然后在内层的_wide_vtable上大做文章将其指向堆上的伪造表最终实现任意函数调用。5. 总结与下篇预告5.1 核心知识点总结缓冲区劫持控制_IO_buf_base和_IO_buf_end等指针可以将fread/fwrite转化为任意地址读写原语。stdout攻击面通过部分覆盖_IO_write_base可以在不破坏结构体整体布局的情况下低成本泄露 Libc 地址。vtable 演进史从无校验到严格限制在__libc_IO_vtables段逼迫攻击者从“伪造假表”转向“滥用合法表内部的复杂逻辑”。5.2 下篇预告在理解了如何伪造结构体和绕过 vtable 之后下一篇我们将把堆漏洞与 IO_FILE 结合起来。精讲House of Orange _IO_FILE 组合在 2.23 环境下的经典复现。探讨exit机制的另一条暗线__exit_funcs与tls_dtor_list劫持。这将在 FSOP 触发条件受限时提供全新的控制流劫持路径。结语IO_FILE 的利用本质是“用合法的框架执行非法的代码”。vtable 校验的加强并没有杀死 FSOP反而逼迫攻击者更深入地理解 glibc 的源码逻辑。掌握_wide_vtable这类“未校验的暗角”是通向现代高阶利用的必经之路。