| 0 | 前言
关于 FILE(_IO_FILE) 结构体的利用,网络上博客比较多,但是很杂很乱,我学习的时候比较迷茫。这篇博客中,我总结了我学习 FILE 结构体的一些经验和一些资料。
✅最近一次更新:2023-11-27
| 1 | 相关知识
参考资料:
| 1.1 | fopen,fread,fwrite
由于 open(), read(), write() 是基于文件描述符(File Descriptor)也就是fd操作的函数,每次调用时都需要切换成内核态进行复杂的File Descriptor Translation,所以开销较大。
因此 glibc 提供了更为高效的 fopen(), fread() 和 fwrite(),以及系列辅助的IO函数。他们都是基于文件指针(File Pointer)即fp操作的函数。
File Pointer 是指向 FILE 结构体的指针(FILE* fp)。
fread 的简单算法是: 当我们需要从文件读数据时,先读到缓冲区并且尽可能填满缓冲区,程序需要的时候从缓冲区读数据。等缓冲区所有数据都被读入或者调用了 fflush 时,再调用 SYS_read 从文件中读入后续的数据填入缓冲区。
fwrite 的简单算法是: 当我们需要写数据到目标文件时,先写到缓冲区等缓冲区满或者调用fflush()时再调用SYS_write写到文件里。
注:上面讲的文件当然也包括了 stdin, stdout, stderr
| 1.2 | FILE结构体
FILE 结构体源码可以点这里查看。
| 1.2.1 | FILE Struct Buffer Pointers
1 | int _flags; /* High-order word is _IO_MAGIC; rest is flags. */ |
| 1.3 | vtable 和 _IO_FILE_plus
_IO_FILE_plus 在 FILE结构体的基础上封装了一个指向_IO_jump_t的指针vtable
1 | struct _IO_FILE_plus |
vtable(virtual table) 是_IO_jump_t的指针其中存放了某些函数的指针
举个例子:如果 vtable 的值存在寄存器 rax 中那么调用 vtable 中的某个偏移为0x68的函数时会执行call *0x68(%rax)即 vtable 里面存的不是函数的入口地址,而是保存函数入口地址的一个地址
| 1.4 | fread, fwrite 原理
IO_FILE 调用的本家函数都是用的虚表,而且全是宏定义,在源码里真的难找
源码分析可以看 Pwn✌️ 鹭雨师傅的博客
| 1.4.1 | fread
主要是 _IO_file_xsgetn
fp->_IO_buf_base为空的情况,表明此时的 FILE 结构体中的指针未被初始化,输入缓冲区未建立,则调用_IO_doallocbuf去初始化指针,建立输入缓冲区have = fp->_IO_read_end - fp->_IO_read_ptr;表示缓冲区中用户还未读入的数据- if want < have ==> memcpy ==> fp->_IO_read_ptr += want; want = 0;return
- else if have>0 缓冲区剩下的不够 ==> memcpy have; want -= have; fp->_IO_read_ptr += have;
- if (buf_end - buf_base) > want 到这说明 have 不够了但是如果 buff 长度够用 ==> call __underflow; continue,调用IO_read把缓冲区填满然后continue,重复
- IO_read(fp, s, size),如果 buff 长度不够,会设置几个指针直接 IO_read,往目标地址(s)读 size ,循环重复
| 1.4.2 | fwrite
类比 fread, 等做题遇到了以后再写吧
| 2 | 利用方法
参考资料:
关于 _IO_FILE 在 glibc2.35 的利用现状思考可以看一下上面贴的anrgy-FSROP,感觉读完作者的分析能对 _IO_FILE 的利用有一些理解。
| 2.1 | 基于vtable的利用思路
_IO_FILE 攻击主要是通过劫持_IO_list_all或者stderr使其指向我们伪造的 fake_file_struct,使得程序在执行IO函数时,会根据我们伪造的 FILE 结构体来选择,这种攻击方式被称为FSOP(File Stream Orienteded Programming)。面向文件流编程或者说文件流导向编程
由于vtable的特殊性,我们通常会在此处做文章劫持程序流,但是在 glibc2.24 加入了 vtable 检查
1 | /* |
根据上面的代码,我们一般只能在一定范围内修改 FILE 结构体的 vtable。
所以,利用方式通常是:根据目前已经发现的存在漏洞的调用链来伪造 FILE 结构体。
_IO_FILE 的利用简单来说就是如何伪造 FILE 结构体,使得程序按照设定的路线执行到目标位置。
| 2.1.1 | 一般伪造结构体有两个主要思路:
根据函数源码设置
在函数体量比较小的时候,这种方法我觉得还是很方便的,不过加上 glibc 的宏定义,有时候这种方法并不太好看其实,可以初步设置某些成员的值。
gdb动态调试
我个人比较喜欢这种,通过动调在出错位置附近看汇编代码,找到通过的方法,其实也是很方便的思路。
所以这里贴一些比较好用的gdb指令
1
2
3
4
5
6
7
8
9
10
11
12bt //back trace查看当前的函数调用链
b* __fxprintf + 171 //断在某个函数的某一行
p *(struct _IO_FILE_plus *) 0x55aaaaaaaaaaaa //把该地址作为 _IO_FILE_plus 结构体指针打印
x/a &_IO_list_all //检查(x) &IO_list_all这个地址的内容,以地址(/a)的形式
x/a &stderr
p/x &_IO_wfile_overflow //以16进制(/x)打印(p) _IO_wfile_overflow的入口地址
search -p 0x7fa16799c410 //查看该地址储存位置,可以用于找vtable
disass //反汇编当前函数
x/30i $rip //检查(x)rip($rip)往下30条指令(/i)
x/s $rdi //检查(x) rdi($rdi)保存的地址指向的值,以字符串的形式(/s)
x/30gx $rax //以四字(/gx) 的形式检查rax 指向的值,显示30项
x/gx $rax+0x68
| 2.1.2 | 触发 IO 流的操作一般有三种
调用
exit()函数
需要设置:
fake_file->mode !=0wide_data->write_ptr > write_basefake_file->lock = locklock的值作为指针,指向的位置值为0且可写触发
__malloc_assert()1
2
3
4
5malloc_assert
--> __fxprintf
-->__vfxprintf
····
--><__GI__IO_wdefault_xsputn+92>: call *0x18(%rax)
另外
malloc_assert()还会调用fflush(stderr)从
main函数返回用不多,遇到的话可以根据前面提的看源码或者动调结合来找。
| 2.2 | house of apple2
目前为止(glibc2.37)我觉得比较实用的链子是house of apple2,具体可以看上面的参考链接。当然,还有很多不错的链子比如 emma、 kiwi、 banana 等等。
这里主要介绍 house of apple2 中基于_IO_wfile_overflow()这个合法vtable函数的利用思路。
| 2.2.1 | 原理分析
虽然在 FILE 结构体中对vtable的合法性进行了检查,但是在 _IO_wide_data结构体中,其对应有一个_wide_vtable成员。
在调用_wide_vtable时没有检查vtable的合法性
1 |
所以只需要调用到_IO_wide_data->_wide_vtable即可。
(fp + 0xa0) 为 fp->_wide_data 域,此处的值为 _IO_wide_data 结构体的地址
当fp->vtable为比如_IO_wfile_overflow()时,则会把fp->_wide_data作为参数,去执行。
| 2.2.2 | 调用链检查绕过
调用链
1
2<_IO_wfile_overflow+608>: call <__GI__IO_wdoallocbuf>
<_IO_wdoallocbuf+43> callq *0x68(%rax) # //rax为_wide_data->_wide_vtable(+0xe0)检查
1
2
3
4
5
6
7
8<_IO_wfile_overflow+27> testb $8, %ah //fake_io_file.flag = 0x800 或者0
<_IO_wfile_overflow+30> jne _IO_wfile_overflow+156
<_IO_wfile_overflow+32> movq 0xa0(%rdi), %rdx //%rdx = wide_data
<_IO_wfile_overflow+39> cmpq $0, 0x18(%rdx) //*(wide_data +0x18) = 0
<_IO_wfile_overflow+44> je _IO_wfile_overflow+608
//······
<_IO_wfile_overflow+608>: call <__GI__IO_wdoallocbuf>
| 2.2.3 | 如何设置 fake FILE 结构体
首先先要明确,我们要伪造三个结构,
FILE结构体_IO_wide_data结构体 以及_wide_vtableFILE 结构体
- 先根据触发 IO 的条件设置
- fake_file->wide_data(+0xa0) = fake_wide_data_addr
- fake_file->vtable = 保存_IO_wfile_overflow的一个地址-0x18(看触发 IO 时是调用的那个偏移)
_IO_wide_data 结构体
- fake_wide_data + 0x18 = 0
- fake_wide_data + 0xe0 = fake_vtable_addr
_wide_vtable
- fake_vtable_addr + 0x68的值为A
- A的值为目标函数
目标函数
- 如果没有沙盒则考虑one_gadget
- 如果one_gadget不行或者开了沙盒,则考虑
setcontext+61结合ROP
| 2.3 | 基于 fread 和 fwrite 任意地址读写
| 2.3.1 | 任意地址写
修改fread(buf,size,nmemb,fp)中的fp->_IO_buf_base为写入起始地址
_IO_buf_end结束地址; 其他 FILE 指针为0
可以利用 pwntools
1 | from pwn import * |
| 2.3.2 | 任意地址读
修改flags = 0x800
fileno = 1 # file descrptor
_IO_read_end = _IO_write_base = target_addr
_IO_write_ptr = target_addr + size
同样可以利用 pwntools
1 | from pwn import * |
| 2.3.3 | 修改 stdout 泄漏 libc
参考资料
IO_FILE泄露libc
简单来说就是设置 flag = 0xfbad1800
然后改小 _IO_write_base
使得输出时,把缓冲区前面的内容输出出来
| 2.3.4 | 修改 stdin 任意地址写
简单来说就是改小 stdin的 _IO_buf_base 使其指向 stdin 结构体的某个偏移,然后再一次输入时,
由于 _IO_read_ptr >=_IO_read_end 会在 underflow 里重新设置缓冲区
1 | //https://elixir.bootlin.com/glibc/latest/source/libio/fileops.c#L460 |
这时,_IO_read_base 被修改成 之前_IO_buf_bas 的值也就是 _IO_2_1_stdin 的某个偏移
然后读入缓冲区修改 _IO_2_2_stdin结构体,把 buf_base 设置成 hook, buf_end 要足够大不然缓冲区不够大
1 | p *stdin |
然后输入缓冲区使得 _IO_read_ptr =_IO_read_end
1 | p *stdin |
然后再在 underflow 再次设置指针,除了 buf_end 都指向 free_hook,
1 | p *stdin |
然后系统调用读入缓冲区, 此时输入目标地址(system),然后 underflow 设置好指针退出
1 | pwndbg> p *stdin |
参考例题 - 2023HITCTF
| 3 | 一些废话
下面是我在学 _IO_FILE 期间写的某种日记,放在最后了。
2023.3.28
在打昨天的校赛的时候才切实体会到了_IO_FILE利用的目的—–辅助堆溢出,还有一些利用方式
_IO_FILE的作用:
在高版本作为
__malloc_hooks的代替手段。在能通过堆任意地址写任意值时能够控制程序流getshell或者读flag提高堆溢出上限。在通过堆漏洞不能做到任意地址写任意值时,比如只能向任意地址写堆地址,通过劫持IO流,达到任意地址写任意值,或者甚至直接getshell
_IO_FILE的利用方式:整体思路是,劫持_IO_list_all,根据题目条件选择合适的调用链再设置FILE结构体关键成员的值,从而达到该链最终指向的漏洞代码(比如可控的函数指针调用)
所以要调整一下IO_FILE的学习思路了,网上各种各样的调用链感觉可以分成上面两类来学,学的时候就主要注意结构体成员是怎么设置的,有源码的看看源码,没有的gdb跟进去看一下原因
2023.4.3
今天为止算是把 _IO_FILE 弄的比较抻展了。
路还远呢 – 2023.11.27
后面几个方向:kernel pwn, 编译原理+chromium&v8, Fuzzing, IOT
先从kernel开始吧
用户态都还没玩明白T_T – 2023.11.27