FSOP基础
FSOP基础
本文适合
已经理解 堆基础、libc 泄露和任意写原语,准备处理 _IO_FILE、stdout/stderr 劫持或 glibc IO 链的 Pwn 学习者。学完你能:判断 FSOP 是否适用,定位 _IO_FILE 关键字段、触发点和 glibc 版本限制,并把堆任意写转成泄露或控制流劫持
FSOP 是 File Stream Oriented Programming,面向文件流结构的利用技术。它利用 glibc 中 _IO_FILE 结构及其虚表、缓冲区字段或链表机制,劫持程序控制流或泄露信息。
FILE 结构是什么
C 标准库中的 FILE* 不是简单指针。
它背后是 glibc 维护的 _IO_FILE 结构,保存了缓冲区、文件描述符、状态标志、函数表等信息。
常见对象包括:
stdin
stdout
stderr如果攻击者能伪造或篡改这些结构,就可能影响后续 IO 行为。
FSOP 需要什么能力
通常需要堆漏洞或任意写能力。
攻击者需要控制 _IO_FILE 结构中的关键字段,或者把伪造结构放到可控内存。
随后触发某个 IO 操作,例如 flush、printf、exit、fclose。
这会让 glibc 按伪造结构执行逻辑。
常见目标
泄露 libc 地址。
劫持 vtable。
触发 _IO_overflow 等函数指针路径。
控制 stdout 缓冲区实现任意读。
结合 setcontext 或 one gadget 完成执行。
不同 glibc 版本保护差异很大。
FSOP 和堆
FSOP 经常和 堆基础 组合。
堆漏洞提供写入或布局能力。
FILE 结构提供可触发的 libc 内部控制流。
这也是为什么 FSOP 通常不是入门第一步,而是堆利用进阶内容。
版本影响
glibc 2.24 之后加强了 vtable 检查。
不同版本中 _IO_FILE 字段布局、hook 可用性、检查强度都不同。
分析 FSOP 题时,必须关注 libc 版本。
_IO_FILE 结构详解
glibc 中 _IO_FILE 的关键字段(glibc 2.23 为例):
struct _IO_FILE {
int _flags; // 标志位,决定 IO 状态
char *_IO_read_ptr; // 读指针
char *_IO_read_end; // 读结束
char *_IO_read_base; // 读基址
char *_IO_write_ptr; // 写指针
char *_IO_write_end; // 写结束
char *_IO_write_base; // 写基址
char *_IO_buf_base; // 缓冲区基址
char *_IO_buf_end; // 缓冲区结束
char *_IO_save_base;
char *_IO_backup_base;
char *_IO_save_end;
struct _IO_marker *_markers;
struct _IO_FILE *_chain; // FILE 链表指针
int _fileno;
int _flags2;
__off_t _old_offset;
unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[1];
_IO_lock_t *_lock;
__off64_t _offset;
struct _IO_codecvt *_codecvt;
struct _IO_wide_data *_wide_data;
struct _IO_FILE *_freeres_list;
void *_freeres_buf;
size_t __pad5;
int _mode;
char _unused2[15 * sizeof(int) - 4 * sizeof(void *) - sizeof(size_t)];
const struct _IO_jump_t *_vtable; // 虚函数表
};重要字段说明:
_flags:控制读写模式、缓冲类型。_IO_write_ptr和_IO_write_base:差值决定输出字符数。_chain:将所有 FILE 结构串成链表。_vtable:指向函数指针表,决定 IO 操作的实际函数。
FSOP 攻击链
一个典型的 FSOP 攻击流程:
第一步:泄露 libc 地址
# 通过篡改 stdout 的 _IO_write_base 来泄露内存
# 构造伪造的 FILE 结构
fake_file = p64(0x0) # _flags
fake_file += p64(0x0) # _IO_read_ptr
fake_file += p64(0x0) # _IO_read_end
fake_file += p64(0x0) # _IO_read_base
fake_file += p64(0x0) # _IO_write_ptr
fake_file += p64(0x7fffffff) # _IO_write_end
fake_file += p64(0x0) # _IO_write_base <- 关键:设小值扩大输出范围第二步:伪造 _IO_FILE_plus
# _IO_FILE_plus 是 _IO_FILE 的扩展,包含 vtable
# 在 glibc 2.24 之前,vtable 没有检查
fake_file = b'/bin/sh\x00'
fake_file = fake_file.ljust(0x60, b'\x00')
fake_file += p64(system_addr) # 覆盖 vtable 中的函数指针第三步:触发 IO 操作
# 触发点通常是:
# - exit() 会遍历所有 FILE 并调用 _IO_FINISH
# - printf/puts 等输出函数
# - fclose()
# - _IO_flush_all_lockpvtable 劫持
glibc 2.24 之前,vtable 检查较弱:
# 旧版本:直接改 vtable 指针
# vtable 指向攻击者控制的内存
# 其中包含伪造的函数指针
# glibc 2.24+:vtable 必须在 __libc_IO_vtables 范围内
# 需要使用其他技巧如 _IO_str_overflow、_IO_wstr_overflowglibc 2.24+ 的 vtable 检查:
// 检查 vtable 地址是否在合法范围内
if ((fp->_mode <= 0 && fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base)
|| _IO_vtable_offset(fp) != 0)
{
// 校验 vtable 偏移
}绕过思路:使用 _IO_str_overflow、_IO_wstr_overflow 等仍在合法范围内的函数。
House of 系列技巧
House of Orange
# 不需要 free,通过修改 top chunk size 触发 free
# 被 free 的 chunk 进入 unsorted bin
# 伪造 FILE 结构利用 unsorted bin attack
# 步骤:
# 1. 堆溢出修改 top chunk size
# 2. malloc 一个大块,旧 top chunk 被 free
# 3. 利用 unsorted bin 的 fd/bk 指针
# 4. 伪造 _IO_FILE_plus 结构
# 5. 触发 exit() 执行 FSOPHouse of Apple
# 利用 _IO_wstrn_overflow
# 在 glibc 2.35+ 仍然可用
# 需要控制 _wide_data 结构House of Cat
# 利用 _IO_wfile_overflow
# 适用于较新 glibc 版本
# 需要构造特定的 _wide_data 和 _IO_FILEglibc 版本差异
关键变化:vtable 无检查,FSOP 最容易
关键变化:加入 vtable 范围检查
关键变化:加入 tcache,移除部分 hook
关键变化:tcache 加入 key 字段防止 double free
关键变化:加强 tcache 和 chunk 检查
关键变化:进一步加强检查
关键变化:移除 __malloc_hook 等 hook
使用 pwntools 构造 FSOP
from pwn import *
# 构造伪造的 FILE 结构
class FakeFILE:
def __init__(self, libc_base):
self.libc = libc_base
self.data = b'\x00' * 0x70 # 基础字段
def set_flags(self, flags):
self.data = p64(flags) + self.data[8:]
def set_write_base(self, addr):
# _IO_write_base 在偏移 0x20
self.data = self.data[:0x20] + p64(addr) + self.data[0x28:]
def set_vtable(self, addr):
# _vtable 在末尾
self.data = self.data[:-8] + p64(addr)
def bytes(self):
return self.data常见误区
- 把 FSOP 简化成”改 vtable”。
- 不看 glibc 版本。
- 不理解触发点,只伪造结构不触发 IO。
- 忽略
_IO_FILE字段之间的一致性。 - 没有先建立堆布局能力。
一句话判断
当堆漏洞已经能改 libc 中的 stdin/stdout/stderr、伪造 _IO_FILE,或者题目输出/退出路径异常依赖 glibc IO 时,就考虑 FSOP。
FSOP 的本质是控制 FILE 结构字段,让 glibc 在 flush、printf、fclose、exit 等触发点上按攻击者布置的状态读写或跳转。
题目中常见信号
- 堆题拿到任意写,但 GOT 因 Full RELRO 不可写。
- 目标是
stdout泄露,或stdin缓冲区劫持。 - libc 版本较老,能利用
_IO_list_all、vtable 或_IO_str_overflow。 - 输出函数调用后泄露异常长内容。
- 程序
exit()、puts()、printf()、fclose()是可控触发点。 - 题解或符号中出现
_IO_FILE_plus、_IO_wide_data、_IO_flush_all_lockp。
核心概念
FSOP 最少要回答三个问题:
改哪个 FILE:stdin / stdout / stderr / fake FILE
改哪些字段:flags、write_base/write_ptr、chain、lock、wide_data、vtable
如何触发:输出、输入、fclose、exit、flush新版本 glibc 对 vtable 范围、链表一致性和 wide data 路径有检查。不要只按“伪造 vtable -> system”老模板做题,先看 libc 版本和可控字段。
最小分析流程
- 确认 libc 版本和
_IO_FILE结构偏移。 - 判断已有原语:任意写、部分写、chunk overlap、UAF edit。
- 定位目标对象:
libc.symbols["_IO_2_1_stdout_"]等。 - 选择目标效果:stdout 泄露、stdin 任意读、vtable/wide_data 执行。
- 布置字段并保持结构一致性,尤其是
_lock、_mode、_wide_data。 - 触发对应 IO 操作。
- 如果 abort,查看是 vtable check、链表检查、锁指针还是字段不一致。
最小验证示例
查看 libc 中标准 FILE:
p &_IO_2_1_stdout_
p *(struct _IO_FILE_plus *)&_IO_2_1_stdout_pwntools 定位:
libc = ELF("./libc.so.6")
stdout = libc.symbols["_IO_2_1_stdout_"] + libc_base
log.info(f"stdout = {hex(stdout)}")验证 stdout 泄露思路时,先只改少数字段,让下一次 puts/printf 输出更多内存;不要一开始就追求执行。
成功标准:触发 IO 后产生可解释的泄露或可控跳转,并能说明字段与触发函数之间的关系。
常见利用 / 解题路线
路线总览:
路线一:stdout 泄露
通过改 _IO_write_base/_IO_write_ptr/_flags 让输出函数泄露 libc、栈或堆地址。
路线二:stdin 缓冲区劫持
改 _IO_buf_base/_IO_buf_end,让下一次输入写到指定地址,形成任意写或扩大写入。
路线三:旧 glibc vtable / _IO_list_all
适合 glibc 2.23 附近老题。新版本通常会被 vtable check 拦截。
路线四:wide_data / House of Apple
适合较新 glibc 中利用 wide character IO 路径,通常需要更强的堆布局和字段控制。
路线五:FSOP + seccomp
如果 execve 被禁,可把 FSOP 作为泄露和任意写阶段,再转 seccomp沙箱 的 ORW。
常见失败原因
- glibc 版本套错:2.23 模板在 2.35 上大概率 abort。
- 字段偏移错:不同架构和 libc 版本
_IO_FILE偏移不同。 - 只伪造不触发:没有执行
exit/flush/fclose/printf等路径。 _lock指针无效:触发加锁时访问非法地址。- vtable 不在允许范围:新版本直接触发检查。
- 结构不一致:write/read 指针关系不满足 IO 函数预期。
迷你案例
堆题有 UAF edit,可覆盖 stdout 前 0x80 字节。Full RELRO,不能改 GOT。
步骤:
泄露 libc -> 定位 _IO_2_1_stdout_
用 tcache poisoning 分配到 stdout 附近
修改 flags 和 write_base/write_ptr
触发 puts 菜单输出
解析泄露出的 libc 地址或栈地址这类 WP 不能只写“打 stdout 泄露”,要说明具体改了哪些字段、为什么下一次输出会读出目标内存,以及 glibc 版本是否支持。