PWN_IO_FILE基础
- 参考博客:[原创]无路远征——GLIBC2.37后时代的IO攻击之道(零)-Pwn-看雪-安全社区|安全招聘|kanxue.com
- 参考博客:关于gdb源码调试环境搭建 - ZikH26 - 博客园
- 参考博客:【技术分享】溢出利用FILE结构体-安全KER - 安全资讯平台
- 参考博客:io file基础 - blog at gets
IO也就是输入输出的意思,但是C语言的输入输出的函数有很多,例如:puts、printf、write、stdin、stdout、scanf、read等与输入输出相关的函数。- 而我们所说的打
IO打的是我们封装到比较上层的函数,比如puts、printf这类的上层封装的函数。read、write这两个是比较底层的,系统调用syscall的输入输出,一般都是调用这两个函数。puts、printf等这些IO函数最后都会通过write这个底层函数与操作系统交互。- 所以我们所说的打
IO,打的就是这种上层输入输出函数。在调用这些函数的时候会经过一些指针、结构体、函数指针等,所以我们通过劫持指针、伪造IO结构体、绕过检查机制从而getshll或者执行shellcode。 - 与
IO相关的都可以在glibc源码中,/path/to/glibc2.23/libio中可以看到。
- 由于高版本的
glibc中hook指针被删除了,所以余下可用的函数指针只有io相关的函数指针了,在高版本堆利用的时候IO利用就成了基础了。
IO_FILE起源及结构体
- 如果出过
pwn题或者做过全缓冲的pwn题就会了解到这个函数。和缓冲区的三种工作模式,无缓冲、行缓冲、全缓冲。
1 | setvbuf(stdin,NULL,_IONBF,0); |
- 为什么要设置这三种模式,这就和
硬件、操作系统、程序三者有关。首先程序要读写硬盘或者是输出到屏幕中,这些都是要通过系统调用syscall,进行write、read系统调用。 - 而何时进行系统调用,这就成为了设计
IO的一个重要问题。如果每一个字节都需要使用syscall系统调用(即无缓冲模式)。向硬件进行读写操作,这就会大大降级操作系统的效率,并且硬件频繁读写也会造成更快的损坏。 - 为了减少这种情况,在设计
IO的时候就会出现导致,设置了缓冲区,要输入的数据或者要输出的数据都会先被放入缓冲区,直到缓冲区满了之后,再进行系统调用,将缓冲区存储的内容写入到屏幕或者其他硬件中(全缓冲),以便提高操作系统的效率,提高硬件的使用寿命,这也就出现了现在的IO_FILE结构体。 - 接下来总结一下
glibc封装的上层函数中与输入输出相关的函数,对于IO的攻击一般就攻击这些IO函数的结构体。- 标准输入函数
gets()、fgets()、scanf()、fscanf()、sscanf()、getc()、fgetc()、getchar()、getline()、getdelim()
- 标准输出函数
printf()、fprintf()、sprintf()、snprintf()、putc()、fputc()、putchar()、puts()
- 文件操作函数:
fopen()、freopen()、fdopen()、fclose()、fflush()、setbuf()、setvbuf()、fread()、fwrite()、fseek()、ftell()、fewind()、rewind()
- 格式化字符串相关函数:
printf()、fprintf()、sprintf()、snprintf()、vprintf()、vfprintf()、vsprintf()、vsnprintf()
- 其他相关函数:
perror()、tmpfile()、clearerr()、feof()、ferror()、stdout、stdout()、stdin()、stderror()
- 标准输入函数
IO_FILE相关动调
-
写介绍几个比较重要的
IO结构体,并且说明这写结构体在glibc2.23源码的什么位置。 -
这里先汇总一下与
IO_FILE相关动调命令,与IO_FILE相关的调试基本上就是打印结构体。可以在gdb中使用p命令打印出结构体的具体存储的值
1 | p stdout |
IO_FILE相关源码
- 这里只介绍一下
IO_FLE重要的相关源码所在的glibc源码文件路径,具体的代码等着调试的时候再看。
glibc2.28之前
glibc/libio/libio.h保存着struct _IO_FILE的定义。glibc/libio/libio.h定义了struct _IO_FILE_plus _IO_2_1_stdin_ 、_IO_2_1_stdout_ 、_IO_2_1_stderr_这三个。glibc/libio/libio.h定义了struct _IO_wide_data这个结构体。glibc/libio/libio.h有着IO_FILE结构体中的int _flags成员中的宏定义glibc/libio/libioP.h保存着struct _IO_FILE_plus结构体的定义。glibc/libio/libioP.h保存着struct _IO_jump_t结构体的定义。glibc/libio/libioP.h定义了struct _IO_FILE_plus *_IO_list_all结构体指针。
IO_FILE基础
_IO_FILE_plus结构及成员
-
对于
_IO_FILE_plus结构,在glibc2.28的时候做出了比较大的变化,其中变化最大的就是其成员_IO_FILE结构体变成了FILE结构体,但是源码仅仅是这样typedef struct _IO_FILE FILE;,结构体成员稍微有点变化。 -
对于
stdin、stdout、stderr,这三个io,是在libc中一开始就定义好的。已经有对应的libc内存中已经存储着它们三个的IO_FILE_plus结构体,程序在每次启动前会经过__libc_start_main进行初始化。 -
当我们使用
fopen()打开一个文件之后,fopen会调用malloc在申请一块堆中的内存,这段堆内存保存着该文件对应的IO_FILE_plus结构体的实例。 -
而
_IO_FILE_plus结构体是如下定义的,其结构体内部包括了来个类型分别为_IO_FILE file和_IO_jump_t *vtable:
1 | // _IO_FILE_plus其实是一个"带虚表的_IO_FILE",这个虚表其实可以支持多态(即不同流类型有不同操作) |
_IO_FILE file的结构体如下:
1 | struct _IO_FILE { |
- 而
stdio、stdin、stderr其实会通过_chain结构体指针使得这三个形成一个链表。先随便找一个能在glibc2.23能调试的程序,看看该libc版本的IO_FILE - 首先先确定
IO_list_all的地址

- 之后查看
_IO_list_all所在地址的内存值,会很惊奇的发现这个_IO_list_all下面其实就是_IO_2_1_stderr,并且_IO_list_all所指向的就是_IO_2_1_stderr,stderr后面是stdout的结构体

- 从调试中我们其实能看到
_IO_FILE_plus结构体的总大小为0xE0,接下来说一下这些偏移。

- 并且会发现
_IO_FILE *chain指向的是stdout,这个IO结构体


- 并且在这俩个结构体下面还存在着俩个三个全局变量,分别指向着对应的
IO结构体

- 所以
IO结构体有如下的逻辑结构

_IO_list_all结构体指针
- 首先来看看
_IO_list_all结构体指针的定义,它是一个_IO_FILE_plus结构体的指针:
1 | extern struct _IO_FILE_plus *_IO_list_all; |
- 其实
_IO_list_all的作用相当于一个链表头,其指向的是_IO_FILE_plus结构体。该指针指向的是_IO_FILE_plus结构体链表的头结点。_IO_list_all与stdin、stdout、stderr三个_IO_FILE_plus结构体的结构如下图所示。

- 当使用
fopen打开一个文件的时候,堆上就会新出现一个_IO_FILE_plus结构体,而该结构体中的_IO_FILE结构体就会通过头插法插入到链表的开头。如下图所示:
注意:fopen打开的文件结构并没有一个全局变量指向新打开的IO_FILE,而是只有_IO_list_all将IO_FILE_plus构成链表

_IO_jump_t结构体
_IO_jump_t结构体如下,其实这就是_IO的一个跳表,这个表里面保存的基本上都是函数指针,并且每个IO_FILE_plus这个结构体中的const struct _IO_jump_t *vtable;这个成员其实都是指向的_IO_jump_t这个结构体所在的位置。
1 | // JUMP_FILED()是一个宏定义 |
- 所以对于
stdio、stdin、stderr这三个更加详细的逻辑结构是如下图所示的

IO_FILE成员_flags
- 对于伪造
IO_FILE首先就是要伪造_flags,该结构体中的成员_flags会有个magic标识该结构体是IO_FILE结构体,如果magic不匹配会导致程序崩溃等,就不能达到伪造IO的目的了。 - 接下来介绍一下
_flags相关的宏定义,其实伪造_flags就将其伪造成(如果是stdout或者一些输出函数)0xFBAD1800即_IO_MAGIC | _IO_IS_APPENDING | _IO_CURRENTLY_PUTTING:
1 |
IO_FILE与文件描述符
-
初步了解了
IO_FILE的一些知识后,会联想到IO_FILE_plus一开始在进程中其实就是有3个对应的分别就是stdin、stdout、stderr。而在操作系统文件描述符中0表示着stdin,1表示着stdout,2表示着stderr。 -
此时就会不禁猜想
文件描述符是不是也与IO_FILE_plus有一定的关系呢?接下来详细说明一下IO_FILE与文件描述符对应的关系。 -
IO_FILE与文件描述符在不同的层面:fd文件描述符是内核级别的整数编号,代表打开的文件。_IO_FILE是glibc用户空间中封装结构,代表C层面的流。_IO_FILE与进程中的struct files_struct的联系其实就是fd文件描述符,fd文件描述符起始相当于offset偏移
-
进程中有一个
struct files_struct,在这个结构体内有一个数组struct file* fd_array[]
1 | //定义在linux内核源码/include/linux/fdtable.h |
- 其中
struct file结构体如下:
1 | // struct file的结构体定义在linux内核源码/include/linux/fs.h |
- 这俩者的联系如下图所示:
- 用户层面
_IO_FILE其实是在用户空间中管理文件输入输出的一个高层封装,当文件内容被载入到虚拟内存空间中对这个缓冲区的操作。 - 而内核态中的
struct file,描述的是一个正在的打开文件,能访问底层硬件即磁盘。 - 并且多个进程如果打开同一个文件,在内核态也只存在着一个
struct file结构体
- 用户层面



