• 参考博客:[原创]无路远征——GLIBC2.37后时代的IO攻击之道(零)-Pwn-看雪-安全社区|安全招聘|kanxue.com
  • 参考博客:关于gdb源码调试环境搭建 - ZikH26 - 博客园
  • 参考博客:【技术分享】溢出利用FILE结构体-安全KER - 安全资讯平台
  • 参考博客:io file基础 - blog at gets
  • IO也就是输入输出的意思,但是C语言的输入输出的函数有很多,例如:putsprintfwritestdinstdoutscanfread等与输入输出相关的函数。
  • 而我们所说的打IO打的是我们封装到比较上层的函数,比如putsprintf这类的上层封装的函数。
    • readwrite这两个是比较底层的,系统调用syscall的输入输出,一般都是调用这两个函数。
    • putsprintf等这些IO函数最后都会通过write这个底层函数与操作系统交互。
    • 所以我们所说的打IO,打的就是这种上层输入输出函数。在调用这些函数的时候会经过一些指针结构体函数指针等,所以我们通过劫持指针伪造IO结构体绕过检查机制从而getshll或者执行shellcode
    • IO相关的都可以在glibc源码中,/path/to/glibc2.23/libio中可以看到。
  • 由于高版本的glibchook指针被删除了,所以余下可用的函数指针只有io相关的函数指针了,在高版本堆利用的时候IO利用就成了基础了。

IO_FILE起源及结构体

  • 如果出过pwn题或者做过全缓冲pwn题就会了解到这个函数。和缓冲区的三种工作模式,无缓冲行缓冲全缓冲
1
2
3
setvbuf(stdin,NULL,_IONBF,0);
setvbuf(stdout,NULL,_IONBF,0);
setvbuf(stderr,NULL,_IONBF,0);
  • 为什么要设置这三种模式,这就和硬件操作系统程序三者有关。首先程序要读写硬盘或者是输出到屏幕中,这些都是要通过系统调用syscall,进行writeread系统调用。
  • 而何时进行系统调用,这就成为了设计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()stdoutstdout()stdin()stderror()

IO_FILE相关动调

  • 写介绍几个比较重要的IO结构体,并且说明这写结构体在glibc2.23源码的什么位置。

  • 这里先汇总一下与IO_FILE相关动调命令,与IO_FILE相关的调试基本上就是打印结构体。可以在gdb中使用p命令打印出结构体的具体存储的值

1
2
3
4
5
6
7
8
p stdout
p _IO_2_1_stdout_

p &_IO_file_jumps
p _IO_file_jumps

p &_IO_list_all
p *(struct _IO_FILE *) _IO_list_all

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
2
3
4
5
6
// _IO_FILE_plus其实是一个"带虚表的_IO_FILE",这个虚表其实可以支持多态(即不同流类型有不同操作)
struct _IO_FILE_plus
{
_IO_FILE file;
const struct _IO_jump_t *vtable; // 这个是一个指向_IO_jump_t结构体的指针,变量名为vtable
};
  • _IO_FILE file的结构体如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
struct _IO_FILE {
int _flags; // 标志位,包括文件状态和打开方式,高位是_IO_FILE的magic,宏定义是这样的#define _IO_MAGIC 0xFBAD0000,在这个宏定义的上下几行都是标志位相关的宏定义

char* _IO_read_ptr; // 读缓冲区当前的读取位置
char* _IO_read_end; // 读缓冲区结束的位置
char* _IO_read_base; // 读缓冲区的基地址
char* _IO_write_base; // 写缓冲区的基地址
char* _IO_write_ptr; // 写缓冲区的当前位置
char* _IO_write_end; // 写缓冲区的结束位置
char* _IO_buf_base; // 缓冲区的基地址
char* _IO_buf_end; // 缓冲区的结束地址


char *_IO_save_base; // 保存缓冲区基地址
char *_IO_backup_base; // 备份缓冲区基地址
char *_IO_save_end; // 保存缓冲区的结束地址

struct _IO_marker *_markers; // 标记指针,用于跟踪缓冲区的读写位置
struct _IO_FILE *_chain; // 比较重要的结构体指针,链接到下一个_IO_FILE文件结构,利用该指针文件结构之间会形成链表
int _fileno; // 文件描述符
int _blksize;
int _flags2; // 额外的文件状态标志
_IO_off_t _old_offset; // 文件偏移(旧版,已弃用)
unsigned short _cur_column; // 当前列号
signed char _vtable_offset; // 虚函数表偏移量
char _shortbuf[1]; // 短缓冲区(用于小量数据的快速操作)
_IO_lock_t *_lock; // 文件锁(用于多线程环境下的文件流操作)
};
  • stdio、stdin、stderr其实会通过_chain结构体指针使得这三个形成一个链表。先随便找一个能在glibc2.23能调试的程序,看看该libc版本的IO_FILE
  • 首先先确定IO_list_all的地址

image-20250720092240647

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

image-20250720092525791

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

image-20250720095933268

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

image-20250720100059540

image-20250720100106833

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

image-20250720100234331

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

image-20250720100712798

_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_allstdin、stdout、stderr三个_IO_FILE_plus结构体的结构如下图所示。

image-20250720100845207

  • 当使用fopen打开一个文件的时候,堆上就会新出现一个_IO_FILE_plus结构体,而该结构体中的_IO_FILE结构体就会通过头插法插入到链表的开头。如下图所示:

注意:fopen打开的文件结构并没有一个全局变量指向新打开的IO_FILE,而是只有_IO_list_all将IO_FILE_plus构成链表

image-20250720101243101

_IO_jump_t结构体

  • _IO_jump_t结构体如下,其实这就是_IO的一个跳表,这个表里面保存的基本上都是函数指针,并且每个IO_FILE_plus这个结构体中的 const struct _IO_jump_t *vtable;这个成员其实都是指向的_IO_jump_t这个结构体所在的位置。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
// JUMP_FILED()是一个宏定义
// 该宏定义是_IO_jump_t的主要成员
#define JUMP_FIELD(TYPE, NAME) TYPE NAME // 其实就是简化声明,例如JUMP_FILED(int, count) 其实就是声明 int count

// 在声明该跳表之前已经定义了好了函数指针,如下
typedef void (*_IO_finish_t) (_IO_FILE *, int);
typedef int (*_IO_overflow_t) (_IO_FILE *, int);
typedef int (*_IO_underflow_t) (_IO_FILE *);
typedef int (*_IO_pbackfail_t) (_IO_FILE *, int);
typedef _IO_size_t (*_IO_xsputn_t) (_IO_FILE *FP, const void *DATA,
_IO_size_t N);
typedef _IO_size_t (*_IO_xsgetn_t) (_IO_FILE *FP, void *DATA, _IO_size_t N);
typedef _IO_off64_t (*_IO_seekoff_t) (_IO_FILE *FP, _IO_off64_t OFF, int DIR,
int MODE);
typedef _IO_off64_t (*_IO_seekpos_t) (_IO_FILE *, _IO_off64_t, int);
typedef _IO_FILE* (*_IO_setbuf_t) (_IO_FILE *, char *, _IO_ssize_t);
typedef int (*_IO_sync_t) (_IO_FILE *);
typedef int (*_IO_doallocate_t) (_IO_FILE *);
typedef _IO_ssize_t (*_IO_read_t) (_IO_FILE *, void *, _IO_ssize_t);
typedef _IO_ssize_t (*_IO_write_t) (_IO_FILE *, const void *, _IO_ssize_t);
typedef _IO_off64_t (*_IO_seek_t) (_IO_FILE *, _IO_off64_t, int);
typedef int (*_IO_close_t) (_IO_FILE *); /* finalize */
typedef int (*_IO_stat_t) (_IO_FILE *, void *);
typedef int (*_IO_showmanyc_t) (_IO_FILE *);
typedef void (*_IO_imbue_t) (_IO_FILE *, void *);

// 该结构体定义了一套函数指针表,所以_IO_jump_t里面存储的,所以该结构体主要成员都是函数指针
// 都是函数指针那就很好了,在低版本中就有劫持_IO_FILE_plus中的vtable,从而触发一些函数指针
struct _IO_jump_t
{
JUMP_FIELD(size_t, __dummy); // 声明变量size_t __dummy,占位符没实际功能
JUMP_FIELD(size_t, __dummy2); // 声明变量size_t __dummy2,占位符没实际功能
JUMP_FIELD(_IO_finish_t, __finish); // 声明函数指针_IO_finish_t __finish,完成操作的函数指针
JUMP_FIELD(_IO_overflow_t, __overflow); // 写缓冲区溢出处理函数指针
JUMP_FIELD(_IO_underflow_t, __underflow); // 读缓冲区欠载处理函数指针
JUMP_FIELD(_IO_underflow_t, __uflow); // 读缓冲区欠载处理函数指针
JUMP_FIELD(_IO_pbackfail_t, __pbackfail); // 处理推回字符的函数指针
/* showmany */
JUMP_FIELD(_IO_xsputn_t, __xsputn); // 写入多个字符的函数指针
JUMP_FIELD(_IO_xsgetn_t, __xsgetn); // 读取多个字符的函数指针
JUMP_FIELD(_IO_seekoff_t, __seekoff); // 按偏移量移动文件指针的函数指针
JUMP_FIELD(_IO_seekpos_t, __seekpos); // 移动文件指针到指定位置的函数指针
JUMP_FIELD(_IO_setbuf_t, __setbuf); // 设置缓冲区的函数指针
JUMP_FIELD(_IO_sync_t, __sync); // 同步文件流的函数指针
JUMP_FIELD(_IO_doallocate_t, __doallocate); // 分配缓冲区的函数指针
JUMP_FIELD(_IO_read_t, __read); // 读取数据的函数指针
JUMP_FIELD(_IO_write_t, __write); // 写入数据的函数指针
JUMP_FIELD(_IO_seek_t, __seek); // 移动文件的函数指针
JUMP_FIELD(_IO_close_t, __close); // 关闭文件的函数指针
JUMP_FIELD(_IO_stat_t, __stat); // 获取文件状态的函数指针
JUMP_FIELD(_IO_showmanyc_t, __showmanyc); // 显示可用字符数的函数指针
JUMP_FIELD(_IO_imbue_t, __imbue); // 设置区域设置信息的函数指针
#if 0
get_column;
set_column;
#endif
};
  • 所以对于stdio、stdin、stderr这三个更加详细的逻辑结构是如下图所示的

image-20250720104635197

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#define _IO_MAGIC 0xFBAD0000           /* Magic number 文件结构体的魔数,用于标识文件结构体的有效性 */
#define _OLD_STDIO_MAGIC 0xFABC0000 /* Emulate old stdio 模拟旧的标准输入输出库(stdio)行为的魔数 */
#define _IO_MAGIC_MASK 0xFFFF0000 /* Magic mask 魔数掩码,用于从 _flags 变量中提取魔数部分 */
#define _IO_USER_BUF 1 /* User owns buffer; don't delete it on close. 用户拥有缓冲区,不在关闭时删除缓冲区 */
#define _IO_UNBUFFERED 2 /* Unbuffered 无缓冲模式,直接进行I/O操作,不使用缓冲区 */
#define _IO_NO_READS 4 /* Reading not allowed 不允许读取操作 */
#define _IO_NO_WRITES 8 /* Writing not allowed 不允许写入操作 */
#define _IO_EOF_SEEN 0x10 /* EOF seen 已经到达文件结尾(EOF) */
#define _IO_ERR_SEEN 0x20 /* Error seen 已经发生错误 */
#define _IO_DELETE_DONT_CLOSE 0x40 /* Don't call close(_fileno) on cleanup. 不关闭文件描述符 _fileno,在清理时不调用 close 函数 */
#define _IO_LINKED 0x80 /* Set if linked (using _chain) to streambuf::_list_all. 链接到一个链表(使用 _chain 指针),用于 streambuf::_list_all */
#define _IO_IN_BACKUP 0x100 /* In backup 处于备份模式 */
#define _IO_LINE_BUF 0x200 /* Line buffered 行缓冲模式,在输出新行时刷新缓冲区 */
#define _IO_TIED_PUT_GET 0x400 /* Set if put and get pointer logically tied. 在输出和输入指针逻辑上绑定时设置 */
#define _IO_CURRENTLY_PUTTING 0x800 /* Currently putting 当前正在执行 put 操作 */
#define _IO_IS_APPENDING 0x1000 /* Is appending 处于附加模式(在文件末尾追加内容) */
#define _IO_IS_FILEBUF 0x2000 /* Is file buffer 是一个文件缓冲区 */
#define _IO_BAD_SEEN 0x4000 /* Bad seen 遇到错误(bad flag set) */
#define _IO_USER_LOCK 0x8000 /* User lock 用户锁定,防止其他线程访问 */

IO_FILE与文件描述符

  • 初步了解了IO_FILE的一些知识后,会联想到IO_FILE_plus一开始在进程中其实就是有3个对应的分别就是stdinstdoutstderr。而在操作系统文件描述符中0表示着stdin1表示着stdout2表示着stderr

  • 此时就会不禁猜想文件描述符是不是也与IO_FILE_plus有一定的关系呢?接下来详细说明一下IO_FILE与文件描述符对应的关系。

  • IO_FILE文件描述符在不同的层面:

    • fd文件描述符是内核级别的整数编号,代表打开的文件。
    • _IO_FILEglibc用户空间中封装结构,代表C层面的流。
    • _IO_FILE进程中的struct files_struct的联系其实就是fd文件描述符,fd文件描述符起始相当于offset偏移
  • 进程中有一个struct files_struct,在这个结构体内有一个数组struct file* fd_array[]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
//定义在linux内核源码/include/linux/fdtable.h
struct files_struct {
/*
* read mostly part
*/
atomic_t count;
bool resize_in_progress;
wait_queue_head_t resize_wait;

struct fdtable __rcu *fdt;
struct fdtable fdtab;
/*
* written part on a separate cache line in SMP
*/
spinlock_t file_lock ____cacheline_aligned_in_smp;
unsigned int next_fd;
unsigned long close_on_exec_init[1];
unsigned long open_fds_init[1];
unsigned long full_fds_bits_init[1];
struct file __rcu * fd_array[NR_OPEN_DEFAULT];
// 文件描述符对应的就是fd_array[]这个数组的索引值.这个数组其实是一个指针数组
// 例如: fd_array[0]对应的就是fd=0,即stdin
};

  • 其中struct file结构体如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// struct file的结构体定义在linux内核源码/include/linux/fs.h
struct file {
union {
struct llist_node fu_llist;
struct rcu_head fu_rcuhead;
} f_u;
struct path f_path;
struct inode *f_inode; /* cached value */
const struct file_operations *f_op;

/*
* Protects f_ep_links, f_flags.
* Must not be taken from IRQ context.
*/
spinlock_t f_lock;
enum rw_hint f_write_hint;
atomic_long_t f_count;
unsigned int f_flags;
fmode_t f_mode;
struct mutex f_pos_lock;
loff_t f_pos;
struct fown_struct f_owner;
const struct cred *f_cred;
struct file_ra_state f_ra;

u64 f_version;
#ifdef CONFIG_SECURITY
void *f_security;
#endif
/* needed for tty driver, and maybe others */
void *private_data;

#ifdef CONFIG_EPOLL
/* Used by fs/eventpoll.c to link all the hooks to this file */
struct list_head f_ep_links;
struct list_head f_tfile_llink;
#endif /* #ifdef CONFIG_EPOLL */
struct address_space *f_mapping;
errseq_t f_wb_err;
errseq_t f_sb_err; /* for syncfs */
} __randomize_layout
__attribute__((aligned(4))); /* lest something weird decides that 2 is OK */
  • 这俩者的联系如下图所示:
    • 用户层面_IO_FILE其实是在用户空间中管理文件输入输出的一个高层封装,当文件内容被载入到虚拟内存空间中对这个缓冲区的操作。
    • 而内核态中的struct file,描述的是一个正在的打开文件,能访问底层硬件即磁盘。
    • 并且多个进程如果打开同一个文件,在内核态也只存在着一个struct file结构体

image-20250720124159746

image-20250720124240918