FILE介绍
FILE是 C 标准 IO 库中用于表示文件流的类型。调用fopen()等函数时,库会创建一个与该流对应的FILE对象,并返回其指针,程序通常使用FILE*来访问和操作这个流。
结构体定义
在glibc-2.26版本中FILE定义如下
struct _IO_FILE {
int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags
/* The following pointers correspond to the C++ streambuf protocol. */
/* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
char* _IO_read_ptr; /* Current read pointer */
char* _IO_read_end; /* End of get area. */
char* _IO_read_base; /* Start of putback+get area. */
char* _IO_write_base; /* Start of put area. */
char* _IO_write_ptr; /* Current put pointer. */
char* _IO_write_end; /* End of put area. */
char* _IO_buf_base; /* Start of reserve area. */
char* _IO_buf_end; /* End of reserve area. */
/* The following fields are used to support backing up and undo. */
char *_IO_save_base; /* Pointer to start of non-current get area. */
char *_IO_backup_base; /* Pointer to first valid character of backup area */
char *_IO_save_end; /* Pointer to end of non-current get area. */
struct _IO_marker *_markers;
struct _IO_FILE *_chain;
int _fileno;
#if 0
int _blksize;
#else
int _flags2;
#endif
_IO_off_t _old_offset; /* This used to be _offset but it's too small. */
#define __HAVE_COLUMN /* temporary */
/* 1+column number of pbase(); 0 is unknown. */
unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[1];
/* char* _save_gptr; char* _save_egptr; */
_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};
struct _IO_FILE_complete
{
struct _IO_FILE _file;
#endif
#if defined _G_IO_IO_FILE_VERSION && _G_IO_IO_FILE_VERSION == 0x20001
_IO_off64_t _offset;
# if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
/* Wide character stream stuff. */
struct _IO_codecvt *_codecvt;
struct _IO_wide_data *_wide_data;
struct _IO_FILE *_freeres_list;
void *_freeres_buf;
# else
void *__pad1;
void *__pad2;
void *__pad3;
void *__pad4;
# endif
size_t __pad5;
int _mode;
/* Make sure we don't get into trouble again. */
char _unused2[15 * sizeof (int) - 4 * sizeof (void *) - sizeof (size_t)];
#endif
};
在glibc内部,标准 C 库的FILE实际上是一个不透明的指针,它在底层对应的是struct _IO_FILE_plus。
struct _IO_FILE:基础结构体,包含缓冲区指针、状态位等。struct _IO_FILE_plus:是对_IO_FILE的包装,它在_IO_FILE的末尾添加了一个指向虚函数表(vtable)的指针。这是劫持控制流的核心。
struct _IO_FILE_plus {
_IO_FILE file;
const struct _IO_jump_t *vtable; // 关键利用点
};
在CTF中,我们不必关心所有字段,只需关注那些能实现内存读写或控制流劫持的关键点。
核心字段
标志位与魔数:_flags
- 偏移:0x0
- 组成:高16位是魔数
0xfbad0000(用于校验),低16位是状态位。
很多系统函数会校验(fp -> _flags & _IO_MAGIC_MASK == _IO_MAGIC)。
在任意地址读利用中,常将其修改为0xfbad1800(绕过检查并设置_IO_IS_APPENDING等标志位),从而强制触发缓冲区刷新。
三组缓冲区指针
glibc使用“基址 (base)”、”当前位置 (ptr)“和”结束位置 (end)“来管理内存。
| 指针类型 | 字段名 | 作用 |
|---|---|---|
| 输入 | _IO_read_ptr_IO_read_end_IO_read_base | 管理从文件读取到内存的数据。当ptr < end时,getc直接从内存取值。 |
| 输出 | _IO_write_base_IO_write_ptr_IO_write_end | 管理待写入文件的数据。当ptr > base时,调用fflush会将这段数据写入内核。 |
| 储备区 | _IO_buf_base_IO_buf_end | 这是整个I/O缓冲区的总边界。base到end的长度决定了缓冲区大小。 |
若要实现任意地址读,通常将_IO_write_base改为目标地址,_IO_write_ptr改为目标地址 + 长度。这样当程序调用stdout的相关函数时,会认为目标地址处的数据是“待刷新的缓冲区内容”而将其打印出来。
链表指针:_chain
偏移:0x68 (64位)
作用:所有的FILE结构体(stdin、stdout、stderr以及fopen打开的文件)都通过这个指针串联在一起,表头是_IO_list_all。
House of Orange 就是通过溢出覆盖_IO_list_all,将其指向受控的堆内存。当程序因报错退出调用_IO_flush_all_lockp时,会遍历该链表,执行受控内存中的vtable。
文件描述符:_fileno
偏移:0x70 作用:对应的系统文件句柄。
将其改为1,原本写入文件的内容会显示在屏幕上;将其改为0,原本写入文件的操作可能变成从屏幕读入(取决于具体逻辑)。
核心机制:vtable (虚函数表)
这是_IO_FILE_plus结构的精髓。它包含了一系列函数指针,处理open、read、write、close、seek等操作。
偏移:0xd8 (64位)
跳转逻辑:当调用fwrite(buf, size, nmemb, fp)时,内核实际上执行的是:fp->vtable->__xsputn(fp, buf, size * nmemb)
_IO_jump_t中的关键逻辑:
__overflow:缓冲区溢出时调用__finish:关闭文件流时调用__xsputn:像puts/fwrite这类输出函数最终会调用它__str_finish:在_IO_str_jumps虚表中,这个位置常被用于劫持RIP到system
一个被劫持的_IO_FILE_plus通常长这样
[ 0x00 ] _flags : 0xfbad1800 (或者 /bin/sh)
[ 0x08 ] _IO_read_ptr : 0x0
...
[ 0x20 ] _IO_write_base : 目标读取地址 (泄露地址时)
[ 0x28 ] _IO_write_ptr : 目标读取地址 + 长度
...
[ 0x68 ] _chain : 指向下一个 FILE 结构
[ 0x70 ] _fileno : 1 (stdout)
...
[ 0xd8 ] vtable : 指向伪造的 jump_t 表或 libc 中的已知虚表
结构体的生命流程
结构体的诞生:fopen
当调用FILE *fp = fopen("file.txt","r");时,底层发生了以下链式反应:
- 分配空间:通过
malloc在堆上分配一个struct _IO_FILE_plus的空间。 - 初始化:设置
_flags(根据模式如”r”,“w”设置)、_fileno(调用内核系统调用open获取) - 挂在虚表:将
vtable指向_IO_file_jumps - 注册虚表:调用
_IO_link_it将这个新结构体挂到全局的_IO_list_all链表上
数据的搬运工:fread/fwrite
这两个函数是缓冲区管理指针(_ptr,_base,_end)的主要操纵者
fread:
- 检查
_IO_read_ptr是否小于_IO_read_end - 如果是,直接从缓冲区拷贝数据给用户,更新
ptr - 如果缓冲区空了,调用
vtable->__underflow触发系统调用读取数据填满缓冲区
fwrite:
- 将数据拷贝到
_IO_write_ptr指向的缓冲区 - 如果缓冲区满了,调用
vtable->__overflow刷新到内核
结构体的销毁:fclose
fclose(fp);不仅仅是关闭文件描述符,它还包含复杂的清理工作
- 刷新缓冲:调用
vtable->__finish确保缓冲区里没写完的数据推送到内核 - 卸载链表:从
_IO_list_all中移除自己 - 释放内存:调用
free(fp)释放堆内存
在fclose过程中,会多次调用vtable中的函数指针,FSOP最常用的触发点就是这里
特殊阶段:系统崩溃/退出:exit和abort
调用exit时,系统会调用_IO_flush_all_lockp顺着_IO_list_all进行遍历,把还没有写完的数据(缓冲区里的数据)全部写进硬盘中。
如果在退出前修改了vtable,就可以劫持程序流。