IO-FILE相关
FILE 在 Linux 系统的标准 IO 库中是用于描述文件的结构,称为文件流。 FILE 结构在程序执行 fopen 等函数时会进行创建,并分配在堆中。
FILE结构
定义一个指向 FILE 结构的指针来接收这个返回值。FILE 结构定义在 libio.h
中,如下所示
struct _IO_FILE { |
进程中的 FILE 结构会通过_chain 域彼此连接形成一个链表,链表头部用全局变量_IO_list_all 表示,通过这个值我们可以遍历所有的 FILE 结构。
在标准 I/O 库中,每个程序启动时有三个文件流是自动打开的:stdin、stdout、stderr。因此在初始状态下,_IO_list_all 指向了一个有这些文件流构成的链表,但是需要注意的是这三个文件流位于 libc.so 的数据段。而我们使用 fopen 创建的文件流是分配在堆内存上的。
我们可以在 libc.so 中找到 stdin\stdout\stderr 等符号,这些符号是指向 FILE 结构的指针,真正结构的符号是:
_IO_2_1_stderr_ |
但是事实上_IO_FILE 结构外包裹着另一种结构_IO_FILE_plus,其中包含了一个重要的指针 vtable 指向了一系列函数指针。
在 libc2.23 版本下,32 位的 vtable 偏移为 0x94
,64 位偏移为 0xd8
struct _IO_FILE_plus |
vtable 是 IO_jump_t 类型的指针,IO_jump_t 中保存了一些函数指针,在后面我们会看到在一系列标准 IO 函数中会调用这些函数指针:
void * funcs[] = { |
相关函数
fread
fread 是标准 IO 库函数,作用是从文件流中读数据,函数原型如下:
size_t fread ( void *buffer, size_t size, size_t count, FILE *stream) ; |
- buffer 存放读取数据的缓冲区。
- size:指定每个记录的长度。
- count: 指定记录的个数。
- stream:目标文件流。
- 返回值:返回读取到数据缓冲区中的记录个数
fread 的代码位于 / libio/iofread.c
中,函数名为\_IO_fread
,但真正的功能实现在子函数_IO_sgetn 中。
_IO_size_t |
在\_IO_sgetn
函数中会调用\_IO_XSGETN
,而\_IO_XSGETN
是_IO_FILE_plus.vtable
中的函数指针,在调用这个函数时会首先取出 vtable 中的指针然后再进行调用。
_IO_size_t |
在默认情况下函数指针是指向_IO_file_xsgetn 函数的
if (fp->_IO_buf_base |
fwrite
fwrite 同样是标准 IO 库函数,作用是向文件流写入数据,函数原型如下
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 来实现写入的功能。
根据前面对\_IO_FILE_plus
的介绍,可知\_IO_XSPUTN
位于_IO_FILE_plus
的 vtable 中,调用这个函数需要首先取出 vtable 中的指针,再跳过去进行调用。
written = _IO_sputn (fp, (const char *) buf, request); |
在\_IO_XSPUTN
对应的默认函数\_IO_new_file_xsputn
中会调用同样位于 vtable 中的_IO_OVERFLOW
/* Next flush the (full) buffer. */ |
\_IO_OVERFLOW
默认对应的函数是_IO_new_file_overflow
if (ch == EOF) |
在_IO_new_file_overflow 内部最终会调用系统接口 write 函数。
fopen
fopen 在标准 IO 库中用于打开文件,函数原型如下
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 函数的初始化操作中,会调用_IO_link_in 把新分配的 FILE 链入_IO_list_all 为起始的 FILE 链表中
void |
之后_fopen_internal 函数会调用_IO_file_fopen 函数打开目标文件,_IO_file_fopen 会根据用户传入的打开模式进行打开操作,总之最后会调用到系统接口 open 函数,这里不再深入。
if (_IO_file_fopen ((_IO_FILE *) new_f, filename, mode, is32) != NULL) |
总结一下 fopen 的操作是
- 使用 malloc 分配 FILE 结构
- 设置 FILE 结构的 vtable
- 初始化分配的 FILE 结构
- 将初始化的 FILE 结构链入 FILE 结构链表中
- 调用系统调用打开文件
fclose
fclose 是标准 IO 库中用于关闭已打开文件的函数,其作用与 fopen 相反。
int fclose(FILE *stream) |
功能:关闭一个文件流,使用 fclose 就可以把缓冲区内最后剩余的数据输出到磁盘文件中,并释放文件指针和有关的缓冲区
fclose 首先会调用_IO_unlink_it 将指定的 FILE 从_chain 链表中脱链
if (fp->_IO_file_flags & _IO_IS_FILEBUF) |
之后会调用_IO_file_close_it 函数,_IO_file_close_it 会调用系统接口 close 关闭文件
if (fp->_IO_file_flags & _IO_IS_FILEBUF) |
最后调用 vtable 中的_IO_FINISH,其对应的是_IO_file_finish 函数,其中会调用 free 函数释放之前分配的 FILE 结构
_IO_FINISH (fp); |
printf/puts
printf 和 puts 是常用的输出函数,在 printf 的参数是以\n
结束的纯字符串时,printf 会被优化为 puts 函数并去除换行符。
puts 在源码中实现的函数是_IO_puts,这个函数的操作与 fwrite 的流程大致相同,函数内部同样会调用 vtable 中的_IO_sputn,结果会执行_IO_new_file_xsputn,最后会调用到系统接口 write 函数。
printf 的调用栈回溯如下,同样是通过_IO_file_xsputn 实现
vfprintf+11 |
下面记录一些攻击技术。
伪造 vtable 劫持程序流程
简介
前面我们介绍了 Linux 中文件流的特性(FILE),我们可以得知 Linux 中的一些常见的 IO 操作函数都需要经过 FILE 结构进行处理。尤其是_IO_FILE_plus 结构中存在 vtable,一些函数会取出 vtable 中的指针进行调用。
因此伪造 vtable 劫持程序流程的中心思想就是针对_IO_FILE_plus 的 vtable 动手脚,通过把 vtable 指向我们控制的内存,并在其中布置函数指针来实现。
因此 vtable 劫持分为两种,一种是直接改写 vtable 中的函数指针,通过任意地址写就可以实现。另一种是覆盖 vtable 的指针指向我们控制的内存,然后在其中布置函数指针。
示例
这里演示了修改 vtable 中的指针,首先需要知道_IO_FILE_plus 位于哪里,对于 fopen 的情况是位于堆内存,对于 stdin\stdout\stderr 是位于 libc.so 中。
int main(void) |
根据 vtable 在_IO_FILE_plus 的偏移得到 vtable 的地址,在 64 位系统下偏移是 0xd8。之后需要搞清楚欲劫持的 IO 函数会调用 vtable 中的哪个函数。关于 IO 函数调用 vtable 的情况已经在 FILE 结构介绍一节给出了,知道了 printf 会调用 vtable 中的 xsputn,并且 xsputn 是 vtable 中的第八项之后就可以写入这个指针进行劫持。
并且在 xsputn 等 vtable 函数进行调用时,传入的第一个参数其实是对应的_IO_FILE_plus 地址。比如这例子调用 printf,传递给 vtable 的第一个参数就是_IO_2_1_stdout_的地址。
利用这点可以实现给劫持的 vtable 函数传參,比如:
|
但是在目前 libc2.23 版本下,位于 libc 数据段的 vtable 是不可以进行写入的。不过,通过在可控的内存中伪造 vtable 的方法依然可以实现利用。
|
我们首先分配一款内存来存放伪造的 vtable,之后修改_IO_FILE_plus 的 vtable 指针指向这块内存。因为 vtable 中的指针我们放置的是 system 函数的地址,因此需要传递参数 “/bin/sh” 或 “sh”。
因为 vtable 中的函数调用时会把对应的_IO_FILE_plus 指针作为第一个参数传递,因此这里我们把 “sh” 写入_IO_FILE_plus 头部。之后对 fwrite 的调用就会经过我们伪造的 vtable 执行 system(“sh”)。
同样,如果程序中不存在 fopen 等函数创建的_IO_FILE 时,也可以选择 stdin\stdout\stderr 等位于 libc.so 中的_IO_FILE,这些流在 printf\scanf 等函数中就会被使用到。在 libc2.23 之前,这些 vtable 是可以写入并且不存在其他检测的。
print &_IO_2_1_stdin_ |
FSOP
简介
FSOP 是 File Stream Oriented Programming 的缩写,根据前面对 FILE 的介绍得知进程内所有的_IO_FILE 结构会使用_chain 域相互连接形成一个链表,这个链表的头部由_IO_list_all 维护。
FSOP 的核心思想就是劫持_IO_list_all 的值来伪造链表和其中的_IO_FILE 项,但是单纯的伪造只是构造了数据还需要某种方法进行触发。FSOP 选择的触发方法是调用_IO_flush_all_lockp,这个函数会刷新_IO_list_all 链表中所有项的文件流,相当于对每个 FILE 调用 fflush,也对应着会调用_IO_FILE_plus.vtable 中的_IO_overflow。
int |
而_IO_flush_all_lockp 不需要攻击者手动调用,在一些情况下这个函数会被系统调用:
- 1、当 libc 执行 abort 流程时
- 2、当执行 exit 函数时
- 3、当执行流从 main 函数返回时
示例
梳理一下 FSOP 利用的条件,首先需要攻击者获知 libc.so 基址,因为_IO_list_all 是作为全局变量储存在 libc.so 中的,不泄漏 libc 基址就不能改写_IO_list_all。
之后需要用任意地址写把_IO_list_all 的内容改为指向我们可控内存的指针,
之后的问题是在可控内存中布置什么数据,毫无疑问的是需要布置一个我们理想函数的 vtable 指针。但是为了能够让我们构造的 fake_FILE 能够正常工作,还需要布置一些其他数据。 这里的依据是我们前面给出的
if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)) |
也就是:
- fp->_mode <= 0
- fp->_IO_write_ptr > fp->_IO_write_base
在这里通过一个示例来验证这一点,首先我们分配一块内存用于存放伪造的 vtable 和_IO_FILE_plus。 为了绕过验证,我们提前获得了_IO_write_ptr、_IO_write_base、_mode 等数据域的偏移,这样可以在伪造的 vtable 中构造相应的数据
|
我们使用分配内存的前 0x100 个字节作为_IO_FILE,后 0x100 个字节作为 vtable,在 vtable 中使用 0x41414141 这个地址作为伪造的_IO_overflow 指针。
之后,覆盖位于 libc 中的全局变量 _IO_list_all,把它指向我们伪造的_IO_FILE_plus。
通过调用 exit 函数,程序会执行 _IO_flush_all_lockp,经过 fflush 获取_IO_list_all 的值并取出作为_IO_FILE_plus 调用其中的_IO_overflow
---> call _IO_overflow |
glibc 2.24 下 IO_FILE 的利用
在 2.24 版本的 glibc 中,全新加入了针对 IO_FILE_plus 的 vtable 劫持的检测措施,glibc 会在调用虚函数之前首先检查 vtable 地址的合法性。首先会验证 vtable 是否位于_IO_vtable 段中,如果满足条件就正常执行,否则会调用_IO_vtable_check 做进一步检查。
/* Check if unknown vtable pointers are permitted; otherwise, |
计算 section_length = __stop___libc_IO_vtables - __start___libc_IO_vtables;
,紧接着会判断 vtable - __start___libc_IO_vtables
的 offset ,如果这个 offset 大于 section_length , 即大于 __stop___libc_IO_vtables - __start___libc_IO_vtables
那么就会调用 _IO_vtable_check()
这个函数。
void attribute_hidden |
如果 vtable 是非法的,那么会引发 abort。
这里的检查使得以往使用 vtable 进行利用的技术很难实现,不过存在新的利用技术。
fileno 与缓冲区的相关利用
在 vtable 难以被利用之后,利用的关注点从 vtable 转移到_IO_FILE 结构内部的域中。 前面介绍过_IO_FILE 在使用标准 IO 库时会进行创建并负责维护一些相关信息,其中有一些域是表示调用诸如 fwrite、fread 等函数时写入地址或读取地址的,如果可以控制这些数据就可以实现任意地址写或任意地址读。
struct _IO_FILE { |
因为进程中包含了系统默认的三个文件流 stdin\stdout\stderr,因此这种方式可以不需要进程中存在文件操作,通过 scanf\printf 一样可以进行利用。
在_IO_FILE 中_IO_buf_base 表示操作的起始地址,_IO_buf_end 表示结束地址,通过控制这两个数据可以实现控制读写的操作。
- Post Title: IO-FILE相关
- Post Author: ggb0n
- Post Link: http://ggb0n.cool/2020/06/14/IO-FILE相关/
- Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.
1.TCTF2020部分题解
2.第五空间pwn题练习
3.堆溢出-Tcache_Attack
4.堆溢出-Housese_Of_XXX
5.堆溢出基础
6.入坑二进制