sev1n75's Tech Blog

路漫漫其修远兮,吾将上下而求索

_IO_FILE利用

| 0 | 前言

关于 FILE(_IO_FILE) 结构体的利用,网络上博客比较多,但是很杂很乱,我学习的时候比较迷茫。这篇博客中,我总结了我学习 FILE 结构体的一些经验和一些资料。

✅最近一次更新:2023-11-27


| 1 | 相关知识

参考资料:

pwn.college-File Struct Exploits

Pwn✌️ 鹭雨师傅的博客

| 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
2
3
4
5
6
7
8
9
10
11
12
13
14
int _flags;  /* High-order word is _IO_MAGIC; rest is flags. */
// flags 用来指明如何设置buffer以及如何与当前文件交互,比如标识 比如当前文件是否可读或可写等

/* The following pointers correspond to the C++ streambuf protocol. */
char *_IO_read_ptr; /* Current read pointer */ // 指向当前缓冲区 read from file but unread by user 数据的开始
char *_IO_read_end; /* End of get area. */ // 指向当前缓冲区 read from file 数据的结尾同时也是 unread by user 数据的结尾
char *_IO_read_base; /* Start of putback+get area. */ // 指向当前缓冲区 read from file 的开头
char *_IO_write_base; /* Start of put area. */ // 指向当前缓冲区 written from user 数据的开头
char *_IO_write_ptr; /* Current put pointer. */ // 指向当前缓冲区 written from user but unwritten to file 数据的开头
char *_IO_write_end; /* End of put area. */ // 指向当前缓冲区 written from user 数据的结尾同时也是 unwritten to file 数据的结尾
char *_IO_buf_base; /* Start of reserve area. */ // 指向缓冲区的开头
char *_IO_buf_end; /* End of reserve area. */ // 指向缓冲区的结尾
// ···
int _fileno; // 对应文件的 fd

| 1.3 | vtable 和 _IO_FILE_plus

_IO_FILE_plusFILE结构体的基础上封装了一个指向_IO_jump_t的指针vtable

1
2
3
4
5
struct _IO_FILE_plus
{
FILE file;
const struct _IO_jump_t *vtable;
};

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 | 利用方法

参考资料:

pwn.college-File Struct Exploits

ctf-wiki FSOP

house of apple2

angry-FSROP

关于 _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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/*
fwrite中的检查,其他的也类似,这是一个叫IO_validate_vtable的函数,但是被定义成了宏?
不是很懂,反正汇编里面没有call而直接写在里面了

# define _IO_JUMPS_FUNC(THIS) (IO_validate_vtable (_IO_JUMPS_FILE_plus (THIS)))

static inline const struct _IO_jump_t *
IO_validate_vtable (const struct _IO_jump_t *vtable){····}

https://elixir.bootlin.com/glibc/glibc-2.37/source/libio/libioP.h#L935
*/
<+131>: mov 0xd8(%rbx),%r15 // %rbx = fake_file ,%r15 = fake_file->vtable
<+138>: lea 0x1959cf(%rip),%rdx # 0x7fce24af8a00 <_IO_helper_jumps> //vtable允许的最小地址
<+145>: lea 0x196730(%rip),%rax # 0x7fce24af9768 //允许的最大地址
<+152>: sub %rdx,%rax // %rax -= %rdx 即rax为偏移最大值
<+155>: mov %r15,%rcx // %rcx = my_vtable
<+158>: sub %rdx,%rcx // %rcx = my_vtable - %rdx = my_offset
<+161>: cmp %rcx,%rax //比较 rax 和 rcx
<+164>: jbe <__GI__IO_fwrite+352> // 以无符号的形式 rax <= rcx 则 jump
//··············
<+352>: call <_IO_vtable_check>

根据上面的代码,我们一般只能在一定范围内修改 FILE 结构体的 vtable。

所以,利用方式通常是:根据目前已经发现的存在漏洞的调用链来伪造 FILE 结构体。

_IO_FILE 的利用简单来说就是如何伪造 FILE 结构体,使得程序按照设定的路线执行到目标位置。

| 2.1.1 | 一般伪造结构体有两个主要思路:

  • 根据函数源码设置

    在函数体量比较小的时候,这种方法我觉得还是很方便的,不过加上 glibc 的宏定义,有时候这种方法并不太好看其实,可以初步设置某些成员的值。

  • gdb动态调试

    我个人比较喜欢这种,通过动调在出错位置附近看汇编代码,找到通过的方法,其实也是很方便的思路。

    所以这里贴一些比较好用的gdb指令

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    bt //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()函数

    图 1 图 1

    需要设置:

    fake_file->mode !=0
    wide_data->write_ptr > write_base
    fake_file->lock = lock lock的值作为指针,指向的位置值为0且可写

  • 触发__malloc_assert()

    1
    2
    3
    4
    5
    malloc_assert
    --> __fxprintf
    -->__vfxprintf
    ····
    --><__GI__IO_wdefault_xsputn+92>: call *0x18(%rax)
    图 2

    另外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
2
#define _IO_WIDE_JUMPS(THIS) \
_IO_CAST_FIELD_ACCESS ((THIS), struct _IO_FILE, _wide_data)->_wide_vtable

所以只需要调用到_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_vtable

  • FILE 结构体

    • 先根据触发 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
2
3
4
5
6
7
from pwn import *
context.arch = 'amd64'
target_addr = 0x7fffa0a0a0a0
fp = FileStructure()
payload = fp.read(target_addr,20)
print(fp)
print(payload)

| 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
2
3
4
5
6
7
from pwn import *
context.arch = 'amd64'
target_addr = 0x7fffa0a0a0a0
fp = FileStructure()
payload = fp.write(target_addr,40)
print(fp)
print(payload)

| 2.3.3 | 修改 stdout 泄漏 libc

参考资料
IO_FILE泄露libc

简单来说就是设置 flag = 0xfbad1800
然后改小 _IO_write_base

使得输出时,把缓冲区前面的内容输出出来

| 2.3.4 | 修改 stdin 任意地址写

参考资料
IO_FILE源码分析:stdin任意写

简单来说就是改小 stdin的 _IO_buf_base 使其指向 stdin 结构体的某个偏移,然后再一次输入时,

由于 _IO_read_ptr >=_IO_read_end 会在 underflow 里重新设置缓冲区

1
2
3
4
5
6
7
8
9
10
11
12
13
//https://elixir.bootlin.com/glibc/latest/source/libio/fileops.c#L460
if (fp->_IO_read_ptr < fp->_IO_read_end) /* 利用的前提 */
return *(unsigned char *) fp->_IO_read_ptr;

........

fp->_IO_read_base = fp->_IO_read_ptr = fp->_IO_buf_base;
fp->_IO_read_end = fp->_IO_buf_base;
fp->_IO_write_base = fp->_IO_write_ptr = fp->_IO_write_end
= fp->_IO_buf_base;

count = _IO_SYSREAD (fp, fp->_IO_buf_base,
fp->_IO_buf_end - fp->_IO_buf_base);

这时,_IO_read_base 被修改成 之前_IO_buf_bas 的值也就是 _IO_2_1_stdin 的某个偏移

然后读入缓冲区修改 _IO_2_2_stdin结构体,把 buf_base 设置成 hook, buf_end 要足够大不然缓冲区不够大

1
2
3
4
5
6
7
8
9
10
11
12
p *stdin
$2 = {
_flags = -72540021,
_IO_read_ptr = 0x7f41180b5900 <_IO_2_1_stdin_+72> "",
_IO_read_end = 0x7f41180b5928 <_IO_2_1_stdin_+72> "",
_IO_read_base = 0x7f41180b5900 <_IO_2_1_stdin_+32> "",
_IO_write_base = 0x0,
_IO_write_ptr = 0x0,
_IO_write_end = 0x0,
_IO_buf_base = 0x7f41180b77a8 <__free_hook> "",
_IO_buf_end = 0x7f41180b78a8 <local_buf+8> "",
_IO_save_base = 0x0,

然后输入缓冲区使得 _IO_read_ptr =_IO_read_end

1
2
3
4
5
6
7
8
9
10
11
12
p *stdin
$3 = {
_flags = -72540021,
_IO_read_ptr = 0x7f41180b5928 <_IO_2_1_stdin_+72> "",
_IO_read_end = 0x7f41180b5928 <_IO_2_1_stdin_+72> "",
_IO_read_base = 0x7f41180b5900 <_IO_2_1_stdin_+32> "",
_IO_write_base = 0x0,
_IO_write_ptr = 0x0,
_IO_write_end = 0x0,
_IO_buf_base = 0x7f41180b77a8 <__free_hook> "",
_IO_buf_end = 0x7f41180b78a8 <local_buf+8> "",
_IO_save_base = 0x0,

然后再在 underflow 再次设置指针,除了 buf_end 都指向 free_hook,

1
2
3
4
5
6
7
8
9
10
11
12
13
p *stdin
$5 = {
_flags = -72540021,
_IO_read_ptr = 0x7f41180b77a8 <__free_hook> "",
_IO_read_end = 0x7f41180b77a8 <__free_hook> "",
_IO_read_base = 0x7f41180b77a8 <__free_hook> "",
_IO_write_base = 0x7f41180b77a8 <__free_hook> "",
_IO_write_ptr = 0x7f41180b77a8 <__free_hook> "",
_IO_write_end = 0x7f41180b77a8 <__free_hook> "",
_IO_buf_base = 0x7f41180b77a8 <__free_hook> "",
_IO_buf_end = 0x7f41180b78a8 <local_buf+8> "",
_IO_save_base = 0x0,
_IO_backup_base = 0x0,

然后系统调用读入缓冲区, 此时输入目标地址(system),然后 underflow 设置好指针退出

1
2
3
4
5
6
7
8
9
10
11
pwndbg> p *stdin
$10 = {
_flags = -72540021,
_IO_read_ptr = 0x7f41180b77a8 <__free_hook> "\240c\323\027A\177",
_IO_read_end = 0x7f41180b77b1 <__malloc_initialize_hook+1> "",
_IO_read_base = 0x7f41180b77a8 <__free_hook> "\240c\323\027A\177",
_IO_write_base = 0x7f41180b77a8 <__free_hook> "\240c\323\027A\177",
_IO_write_ptr = 0x7f41180b77a8 <__free_hook> "\240c\323\027A\177",
_IO_write_end = 0x7f41180b77a8 <__free_hook> "\240c\323\027A\177",
_IO_buf_base = 0x7f41180b77a8 <__free_hook> "\240c\323\027A\177",
_IO_buf_end = 0x7f41180b78a8 <local_buf+8> "",

参考例题 - 2023HITCTF


| 3 | 一些废话

下面是我在学 _IO_FILE 期间写的某种日记,放在最后了。

2023.3.28

在打昨天的校赛的时候才切实体会到了_IO_FILE利用的目的—–辅助堆溢出,还有一些利用方式

  • _IO_FILE的作用:

    1. 在高版本作为__malloc_hooks的代替手段。在能通过堆任意地址写任意值时能够控制程序流getshell或者读flag

    2. 提高堆溢出上限。在通过堆漏洞不能做到任意地址写任意值时,比如只能向任意地址写堆地址,通过劫持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