_IO_2_1_stdout 泄露libc
参考资料:https://www.cnblogs.com/Langx/articles/19089433
前言:尽管我们前面已经学了很多关于 __IO_FILE 的知识,不过我们这里依旧从头开始了解 _IO_2_1_stdout。
_IO_FILE 结构
FILE 在 linux 系统的标准 IO 库使用来表述文件结构,称之为文件流。这里所提及的”流”其实是一种抽象的概念。无论是硬件还是软件其实都没有”流”这一说,只是人们为了便于表述数据的流向而创造的名称。比如说当我们要输出磁盘中记录的数据,那么在计算机中首先会将磁盘中的数据加载进内存,那么磁盘 -> 内存这种流向就被抽象地叫做”流”。
FILE结构定义在libio.h:
1 | struct _IO_FILE { |
一个进程中的 FILE 结构会通过 _chain 域彼此连接形成一个链表,链表的头部用局部变量 _IO_list_all 表示,通过这个值我们能遍历所有的 FILE 结构。

在标准 I/O 库中,每个程序启动时 stdin、stdout、stderr 这三个文件流会自动打开。因此在初始状态下,_IO_list_all 指向了一个由这些文件流构成的链表,但是这三个文件流是位于 libc.so 的数据段上,而我们使用 fopen 创建的文件流是分配到堆内存上的。
_IO_FILE_plus 结构体
FILE 结构体外还报过来另一种结构 _IO_FILE_plus,其中包含了一个重要的指针 vtable 虚表指向了一系列函数指针:
1 | struct _IO_FILE_plus |
- 虚函数表是一个储存在内存中的表格,其中包含了类中所有虚函数的指针,每个类都有自己的虚函数表。当调用虚函数时,编译器通过虚函数表来确定应该调用哪个函数的实现。
这里可以看见 vtable 是 _IO_jump_t 类型的指针,IO_jump_t 中保存了一些函数指针,在后面我们会看到在一系列标准 IO 函数中会调用这些函数指针。也就是说,如果使用 _IO_FILE_plus 去定义一个结构体指针的话,我们既可以使用 _IO_FILE 中的结构体成员变量,也能使用 _IO_jump_t 中的函数指针。
_IO_jump_t
1 | struct _IO_jump_t |
_flags 规则:
_flag 是 _IO_FILE 结构体中的第一个成员变量,这个成员变量在利用 _IO_2_1_stdoue 泄露 libc 的时候很重要。
_flag 的高两位字节是由 libc 规定的,不同的 libc 可能存在差异,但基本上都是 0xfbad0000。
高两位字节的作用是作为一个标识,表示这是一个什么文件。而低两位的位数则决定了程序的运行状态:
_flags 低两位的规则如下:
1 |
在执行流程中一般会将 _flag 和定义常量进行按位与预算,并根据与运算的结构进行判断如何执行:
puts() 函数的执行流程
_IO_puts --> _IO_new_file_xsputn
puts 函数在源码中的表现形式为 _IO_puts:
1 | int |
这里可以看到 _IO_puts 在过程当中调用了一个叫做 _IO_sputn 的函数,[ _IO_fwrite 也会调用这个 ],_IO_sputn 其实是一个宏,他的作用就是调用 IO_2_1_stdout 中的 vtable 所指向的 _xsputn,也就是 _IO_new_file_xsputn 函数。
_IO_new_file_xsputn --> _IO_OVERFLOW:
1 | size_t |
首先进入函数之后判断输出缓冲区还有多少空间,这里是由 _IO_write_end - _IO_write_base 得来的,这两个是 FILE 结构体中的两个成员遍历那个,分别是输出结束地址和真实输出地址。
关键代码:
1 | 1335 if (__IO_OVERFLOW (f, EOF) == EOF) |
经过上述最后一步的判断,如果还有剩余则说明输出缓冲区未建立或者空间已满,那么就需要通过 _IO_OVERFLOW 函数来建立或清空缓冲区,这个函数主要是实现刷新缓冲区或建立缓冲区的功能。在 vtable 为 _overflow
_IO_new_file_overflow --> _IO_do_write
1 | int |
上述代码关键在于 :
_IO_do_write (f, f->_IO_write_base,f->_IO_write_ptr - f->_IO_write_base) ,我们需要成功执行 _IO_do_write()函数,这个函数作用是调用write输出输出缓冲区,传入的参数分别为:stdout结构体、_IO_write_base(输出缓冲区起始地址)和size(_IO_write_end - _IO_write_base计算得来)
这时,我们可以事先在stdout的_IO_write_base的位置部署要输出的起始地址,那么再去利用_IO_do_write函数,即可打印部分内存地址,打印出来的内容就包含我们所需要泄露的libc
为了执行_IO_do_write()函数,我们得绕过前面的检查:
第一个检查:
1 | if (f->_flags & _IO_NO_WRITES) /* SET ERROR */ |
这里判断 _flags 的标志位是否包含 _IO_NO_WRITES;
1 |
为了通过这个检查,我们将此处的运算计算为假,所以我们只需要将 _flags 设置为 0xfbad0000 即可 。
1 | _flag=0xFBAD0000 --> 11111011101011010000000000000000 (第三位为0即可) |
第二个检查:
1 | if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0 || f->_IO_write_base == NULL){...} |
由于我们会先覆盖 _IO_write_base 的地址,所以 f -> _IO_write_base NULL 必定为假,接下来我们令( f -> _flags & _IO_CURRENTLY_PUTTING )== 0 为假即可,即设置 _flags = 0xfbad0800 。
1 |
|
第三个检查:
1 | if (ch == EOF) |
由于前面传进来的参数就是 EOF 所以不需要去管他,在绕过这些检查之后,
我们就成功进入了 _IO_do_write()函数
_IO_new_do_write --> new_do_write
1 | int |
我们进入 _IO_do_write()函数之后就会进入 _IO_new_do_write 函数,该函数只是调用了 _new_do_write 函数,参数分别为 stdout 结构体,输出缓冲区其实地址,输出长度
跟进 _new_do_write 函数:
new_do_write --> _IO_SYSWRITE:
1 | static size_t |
count = _IO_SYSWRITE (fp, data, to_do);首先明确目标是进入这个函数,该函数会执行系统调用write。
1 | if (fp->_flags & _IO_IS_APPENDING) |
接下来就是考虑这两个判断语句,我们只需要去满足 if (fp->_flags & _IO_IS_APPENDING)这个判断语句,去执行 count = _IO_SYSWRITE (fp, data, to_do);,因为另一个条件判断绕过是比较难的。
我们只需要将 _flags 设置为 0xfbad1000 即可
1 |
|
接下来就可以执行 _IO_SYSWRITE (fp, data, to_do)函数打印出我们一开始设置的要输出的起始地址,从而达到泄露libc的目的了。
总结:
我们需要满足以下条件来执行_IO_SYSWRITE (fp, data, to_do):
1 |
|
- 设置_IO_write_base指向想要泄露的位置,_IO_write_ptr指向泄露结束的地址(不需要一定设置指向结尾,stdout结构中自带地址也足够泄露libc)
整个函数的调用流程:
1 |
|
更新: 2026-04-02 20:51:12
原文: https://www.yuque.com/idcm/wnemg9/pat0v7g2626mhiv4