glibc FILE 结构剖析:Linux 标准 I/O 文件流、vtable 调用链与 Pwn 利用基础

发布时间:2026/9/25 13:07:42
glibc FILE 结构剖析:Linux 标准 I/O 文件流、vtable 调用链与 Pwn 利用基础 文档网络安全教程【免费下载链接】ctf-wikiCome and join us, we need you!项目地址https://gitcode.com/gh_mirrors/ct/ctf-wiki点击查看免费下载导读本篇文章以 CTF-Wiki 中「Linux 用户态 Pwn」章节的 FILE 结构介绍为基础系统讲解 glibc 标准 I/O 库中FILE_IO_FILE结构的完整布局、_IO_FILE_plus与 vtable 的封装关系以及fread、fwrite、fopen、fclose、printf/puts等常见 I/O 函数在 libc 内部的调用链。读者学完后将能看懂FILE结构中每个关键字段的含义与偏移理解为什么标准 I/O 函数的每次读写最终都会经过 vtable 中的函数指针并掌握后续伪造 vtable、FSOP 等利用手法的底层前提。FILE 结构标准 I/O 库中的文件流在 Linux 系统的标准 I/O 库中FILE是用于描述文件的结构也称为文件流。FILE结构在程序执行fopen等函数时会进行创建并分配在堆上。我们常定义一个指向FILE结构的指针来接收这个返回值FILE *fp fopen(123.txt, rw);FILE结构定义在 libc 源码的libio.h中其核心成员如下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 putbackget 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 its too small. */ #define __HAVE_COLUMN /* temporary */ /* 1column 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; size_t __pad5; int _mode; /* Make sure we dont get into trouble again. */ char _unused2[15 * sizeof (int) - 4 * sizeof (void *) - sizeof (size_t)]; #endif };关键字段速查字段偏移64 位 libc 2.23含义_flags0x00高 16 位是_IO_MAGIC其余为状态标志位_IO_read_ptr0x08当前读指针_IO_read_end0x10读区结束地址_IO_read_base0x18读区含 putback起始地址_IO_write_base0x20写区起始地址_IO_write_ptr0x28当前写指针_IO_write_end0x30写区结束地址_IO_buf_base0x38缓冲保留区起始地址_IO_buf_end0x40缓冲保留区结束地址_chain0x68指向链表中的下一个FILE结构_fileno0x70文件描述符_mode0xc0流模式标志上表偏移适用于常见 64 位 glibc2.23环境不同架构与版本下请以实际调试结果为准。在 CTF-Wiki 的 exploit-in-libc2.24.md 一节中这些偏移被直接用于构造 fake FILE是后续利用的基础。_IO_list_all与三个默认文件流进程中的FILE结构会通过_chain域彼此连接形成一个链表链表头部用全局变量_IO_list_all表示。通过这个值我们可以遍历所有的FILE结构fp (_IO_FILE *) _IO_list_all; while (fp ! NULL) { ... fp fp-_chain; }在标准 I/O 库中每个程序启动时有三个文件流是自动打开的stdin、stdout、stderr。因此在初始状态下_IO_list_all指向一个由这些文件流构成的链表。需要注意的是这三个文件流位于 libc.so 的数据段而我们使用fopen创建的文件流是分配在堆内存上的。可以在 libc.so 中找到stdin、stdout、stderr等符号但这些符号只是指向FILE结构的指针真正结构的符号是_IO_2_1_stderr_ _IO_2_1_stdout_ _IO_2_1_stdin__IO_FILE_plus包裹 FILE 的外层结构事实上_IO_FILE结构外包裹着另一种结构_IO_FILE_plus其中包含了一个重要的指针vtable指向一系列函数指针struct _IO_FILE_plus { _IO_FILE file; IO_jump_t *vtable; }在 libc 2.23 版本下32 位的 vtable 偏移为0x9464 位偏移为0xd8。因此拿到任意_IO_FILE_plus地址后加上0xd864 位即可取出 vtable 指针。vtable是IO_jump_t类型的指针IO_jump_t中保存了一些函数指针。在后面我们会看到在一系列标准 I/O 函数中会调用这些函数指针。vtable 中函数指针的布局如下注意数组下标从 0 开始因此下标 2 对应第 3 项void * funcs[] { 1 NULL, // extra word 2 NULL, // DUMMY 3 exit, // finish 4 NULL, // overflow 5 NULL, // underflow 6 NULL, // uflow 7 NULL, // pbackfail 8 NULL, // xsputn #printf 9 NULL, // xsgetn 10 NULL, // seekoff 11 NULL, // seekpos 12 NULL, // setbuf 13 NULL, // sync 14 NULL, // doallocate 15 NULL, // read 16 NULL, // write 17 NULL, // seek 18 pwn, // close 19 NULL, // stat 20 NULL, // showmanyc 21 NULL, // imbue };此处的pwn是原文演示用的占位注释实际 glibc 中对应_IO_new_file_close等实现函数。重点在于printf会走到xsputn下标 7、fread会走到xsgetn下标 8、fclose会走到finish下标 2这些下标在后面的伪造 vtable 利用中至关重要。fread底层调用链中的 vtable 跳转fread是标准 I/O 库函数作用是从文件流中读数据函数原型如下size_t fread ( void *buffer, size_t size, size_t count, FILE *stream) ;buffer存放读取数据的缓冲区。size指定每个记录的长度。count指定记录的个数。stream目标文件流。返回值返回读取到数据缓冲区中的记录个数。fread的代码位于 libc 源码的/libio/iofread.c中函数名为_IO_fread但真正的功能实现在子函数_IO_sgetn中_IO_size_t _IO_fread (buf, size, count, fp) void *buf; _IO_size_t size; _IO_size_t count; _IO_FILE *fp; { ... bytes_read _IO_sgetn (fp, (char *) buf, bytes_requested); ... }在_IO_sgetn函数中会调用_IO_XSGETN而_IO_XSGETN是_IO_FILE_plus.vtable中的函数指针。在调用这个函数时会首先取出 vtable 中的指针然后再进行调用_IO_size_t _IO_sgetn (fp, data, n) _IO_FILE *fp; void *data; _IO_size_t n; { return _IO_XSGETN (fp, data, n); }在默认情况下该函数指针指向_IO_file_xsgetn函数。其内部逻辑会检查缓冲区状态例如当缓冲区中已有数据且请求长度小于缓冲剩余空间时会直接通过__underflow取数据if (fp-_IO_buf_base want (size_t) (fp-_IO_buf_end - fp-_IO_buf_base)) { if (__underflow (fp) EOF) break; continue; }这段代码的含义是优先使用_IO_buf_base到_IO_buf_end之间的缓冲区缓冲区不足时才触发底层read系统调用。这也解释了为什么在 libc 2.24 之后的利用中攻击者通过篡改_IO_buf_base与_IO_buf_end就能实现任意地址读写详见 exploit-in-libc2.24.md。fwrite经由 xsputn 与 overflow 的写路径fwrite同样是标准 I/O 库函数作用是向文件流写入数据函数原型如下size_t fwrite(const void* buffer, size_t size, size_t count, FILE* stream);buffer是一个指针对fwrite来说是要写入数据的地址。size要写入内容的单字节数。count要进行写入size字节的数据项的个数。stream目标文件指针。返回值实际写入的数据项个数count。fwrite的代码位于/libio/iofwrite.c中函数名为_IO_fwrite。在_IO_fwrite中主要是调用_IO_XSPUTN来实现写入的功能written _IO_sputn (fp, (const char *) buf, request);根据前面对_IO_FILE_plus的介绍可知_IO_XSPUTN位于_IO_FILE_plus的 vtable 中调用这个函数需要首先取出 vtable 中的指针再跳过去进行调用。在_IO_XSPUTN对应的默认函数_IO_new_file_xsputn中会调用同样位于 vtable 中的_IO_OVERFLOW/* Next flush the (full) buffer. */ if (_IO_OVERFLOW (f, EOF) EOF)_IO_OVERFLOW默认对应的函数是_IO_new_file_overflowif (ch EOF) return _IO_do_write (f, f-_IO_write_base, f-_IO_write_ptr - f-_IO_write_base); if (f-_IO_write_ptr f-_IO_buf_end ) /* Buffer is really full */ if (_IO_do_flush (f) EOF) return EOF;在_IO_new_file_overflow内部最终会调用系统接口write函数。可见fwrite的完整链路为_IO_fwrite → _IO_sputnvtable[xsputn]→ _IO_new_file_xsputn → _IO_OVERFLOWvtable[overflow]→ _IO_new_file_overflow → write。这条链路上有两个 vtable 函数指针xsputn、overflow被间接调用这也是伪造 vtable 时最容易下手的位置。fopenFILE 的堆分配与链表挂载fopen在标准 I/O 库中用于打开文件函数原型如下FILE *fopen(char *filename, *type);filename目标文件的路径。type打开方式的类型。返回值返回一个文件指针。在fopen内部会创建FILE结构并进行一些初始化操作。首先在fopen对应的函数__fopen_internal内部会调用malloc函数分配FILE结构的空间——因此我们可以获知FILE结构是存储在堆上的*new_f (struct locked_FILE *) malloc (sizeof (struct locked_FILE));之后会为创建的FILE初始化 vtable并调用_IO_file_init做进一步初始化_IO_JUMPS (new_f-fp) _IO_file_jumps; _IO_file_init (new_f-fp);在_IO_file_init函数的初始化操作中会调用_IO_link_in把新分配的FILE链入以_IO_list_all为起始的FILE链表中void _IO_link_in (fp) struct _IO_FILE_plus *fp; { if ((fp-file._flags _IO_LINKED) 0) { fp-file._flags | _IO_LINKED; fp-file._chain (_IO_FILE *) _IO_list_all; _IO_list_all fp; _IO_list_all_stamp; } }之后__fopen_internal函数会调用_IO_file_fopen函数打开目标文件。_IO_file_fopen会根据用户传入的打开模式进行打开操作最后会调用到系统接口open函数if (_IO_file_fopen ((_IO_FILE *) new_f, filename, mode, is32) ! NULL) return __fopen_maybe_mmap (new_f-fp.file);总结一下fopen的操作是使用malloc分配FILE结构设置FILE结构的 vtable默认为_IO_file_jumps初始化分配的FILE结构将初始化的FILE结构链入FILE结构链表中调用系统调用open打开文件。fclose脱链、关文件与释放fclose是标准 I/O 库中用于关闭已打开文件的函数其作用与fopen相反int fclose(FILE *stream)功能关闭一个文件流使用fclose就可以把缓冲区内最后剩余的数据输出到磁盘文件中并释放文件指针和有关的缓冲区。fclose首先会调用_IO_unlink_it实际宏为_IO_un_link将指定的FILE从_chain链表中脱链if (fp-_IO_file_flags _IO_IS_FILEBUF) _IO_un_link ((struct _IO_FILE_plus *) fp);之后会调用_IO_file_close_it函数_IO_file_close_it会调用系统接口close关闭文件if (fp-_IO_file_flags _IO_IS_FILEBUF) status _IO_file_close_it (fp);最后调用 vtable 中的_IO_FINISH其对应的是_IO_file_finish函数其中会调用free函数释放之前分配的FILE结构_IO_FINISH (fp);printf / puts殊途同归的 xsputn 调用printf和puts是常用的输出函数。在printf的参数是以\n结束的纯字符串时printf会被编译器优化为puts函数并去除换行符。puts在源码中实现的函数是_IO_puts这个函数的操作与fwrite的流程大致相同函数内部同样会调用 vtable 中的_IO_sputn结果会执行_IO_new_file_xsputn最后会调用到系统接口write函数。printf的调用栈回溯如下同样是通过_IO_file_xsputn实现vfprintf11 _IO_file_xsputn _IO_file_overflow funlockfile _IO_file_write write这条调用链揭示了两个对利用有价值的事实只要程序执行了任何输出操作printf/puts/fwrite就必然经过_IO_FILE_plus的 vtable因此不需要程序里显式存在文件操作仅靠这三个默认流就能触发 vtable 中的函数指针调用 vtable 函数时传入的第一个参数是_IO_FILE_plus自身的地址攻击者可以利用这个参数向被劫持的函数如system传参。从结构到利用vtable 劫持与 FSOP 的关联理解FILE结构与 vtable 调用链的最终目的是服务于 Linux Pwn 中的文件流利用。CTF-Wiki 在本篇之后紧接着的三个小节正是基于本篇结构的直接延伸伪造 vtable 劫持程序流程核心思想是针对_IO_FILE_plus的 vtable 动手脚。一种方式是直接改写 vtable 中的函数指针需要任意地址写另一种是覆盖 vtable 指针使其指向我们控制的堆内存并在其中布置函数指针。64 位下 vtable 偏移0xd8xsputn是 vtable 中下标 7 的指针。由于 vtable 函数调用时第一个参数就是_IO_FILE_plus地址可以把sh写入FILE头部让fwrite最终执行system(sh)#define system_ptr 0x7ffff7a52390; int main(void) { FILE *fp; long long *vtable_addr,*fake_vtable; fpfopen(123.txt,rw); fake_vtablemalloc(0x40); vtable_addr(long long *)((long long)fp0xd8); //vtable offset vtable_addr[0](long long)fake_vtable; memcpy(fp,sh,3); fake_vtable[7]system_ptr; //xsputn fwrite(hi,2,1,fp); }FSOPFile Stream Oriented Programming核心思想是劫持_IO_list_all的值来伪造链表和其中的_IO_FILE项通过_IO_flush_all_lockp触发。该函数会遍历_IO_list_all链表对每个满足fp-_mode 0 fp-_IO_write_ptr fp-_IO_write_base条件的FILE调用 vtable 中的_IO_overflow。_IO_flush_all_lockp无需攻击者手动调用在 libc 执行abort流程、执行exit函数、或执行流从main返回时都会被系统调用。glibc 2.24 下的 IO_FILE 利用glibc 2.24 引入了IO_validate_vtable检查在调用虚函数之前验证 vtable 地址是否位于__libc_IO_vtables段中非法则调用_IO_vtable_check并触发 abort。这使得传统伪造 vtable 手法失效利用关注点转向_IO_FILE结构内部的字段如_IO_buf_base/_IO_buf_end实现任意读写以及不在检查范围内的_IO_str_jumpsvtable其overflow/finish函数内部存在可被利用的malloc/memcpy/free/函数指针调用原语。小结本篇梳理了 glibc 标准 I/O 库中FILE结构的完整画像FILE_IO_FILE是描述文件流的结构fopen创建时分配在堆上三个默认流_IO_2_1_stdin_/_IO_2_1_stdout_/_IO_2_1_stderr_位于 libc.so 数据段所有FILE通过_chain组成以全局变量_IO_list_all为头部的链表_IO_FILE_plus在_IO_FILE之外增加了vtable指针64 位 libc 2.23 偏移0xd8vtable 中按固定下标保存了finish、overflow、xsputn、xsgetn、setbuf等函数指针fread、fwrite、fopen、fclose、printf/puts的底层实现都会间接调用 vtable 中的函数指针且调用时第一个参数是_IO_FILE_plus自身地址。掌握了这些结构布局与调用链之后无论是伪造 vtable、篡改_IO_buf_base实现任意读写还是通过_IO_str_jumps构造 getshell 原语都能做到知其然亦知其所以然。后续三个进阶小节fake-vtable-exploit.md、fsop.md、exploit-in-libc2.24.md均建立在本篇基础之上建议按顺序阅读并配合 gdb/pwndbg 实际调试验证各字段偏移。赞分享文档网络安全教程【免费下载链接】ctf-wikiCome and join us, we need you!项目地址https://gitcode.com/gh_mirrors/ct/ctf-wiki点击查看免费下载相关推荐Okio Benchmarks使用 JMH 微基准测试剖析 Okio 缓冲区与 I/O 性能Okio Benchmarks使用 JMH 微基准测试剖析 Okio 缓冲区与 I/O 性能 Okio 是一个为 Android、Java 与 Kotlin后端跨平台gVisor 文件系统 I/O 如何调优overlay2、directfs 与 file-access 缓存配置gVisor 文件系统 I/O 如何调优overlay2、directfs 与 file access 缓存配置 在 gVisor 沙箱里跑 I/O 密集负载云原生容器运行时操作系统应用安全Hermes 基准测试剖析single-file widgets 单文件基准的构造、变体与对照实现Hermes 基准测试剖析single file widgets 单文件基准的构造、变体与对照实现 本篇文章围绕 Hermes 仓库中 benchmarks/语言运行时编译器移动开发上一篇5步解锁专业功能Wand-Enhancer本地配置文件修改方案深度解析下一篇3分钟快速掌握Windows平台NCM音频文件批量转换终极指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考