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
结构体
- 用户层面