_IO_FILE利用

sky123师傅博客:http://t.csdnimg.cn/O8y8S,从IO_FILE的结构、原理到利用,写得极其详细易懂

hollk师傅:好好说话之IO_FILE利用(1):利用_IO_2_1_stdout泄露libc_libc泄露方式-CSDN博客

_IO_FILE结构 在2.31中定义在了struct_FILE.h头文件中

参照sky123师傅的贴图仿作出一幅IO_FILE结构关系图

image-20240608202007286

1.FILE结构

_IO_FILE结构在较低版本的libc中定义在glibc/libio/libio.h,较高版本定义在glibc/libio/bits\types\struct_FILE.h中,__IO_FILE结构体中存储缓冲区相关的成员变量。

_IO_list_all指向最后创建的FILE结构(初始化后指向__IO_2_1_stderr),各FILE结构通过_chain域链接形成一个单项链表。

_IO_FILE_plus中包含了_IO_FILE结构以及名为vtable的 _IO_jump_t 结构的虚表,所有FILE结构体的虚表指针均指向向虚表_IO_file_jumps,在进行IO操作时,都会调用到该结构体中的函数,即作为公共函数表使用(函数多态性呈现)。

进行IO操作时,大致流程是通过访问__IO_FILE结构体中的缓冲区相关变量,通过调用虚表中指定的一系列函数来完成操作。

那么我们的利用大概分为四个方向:a.__IO_FILE结构体成员变量;b.虚表;c.__IO_FILE_plus;

过多关于FILE结构的内容就不过多描述了,不太想过多的炒冷饭。直接讲讲利用了。

2.利用

2.1 __IO_FILE结构体成员变量

__IO_2_1_stdout成员变量利用

通过伪造stdout结构体中的成员变量来达成任意地址读的目的。

image-20240609141531616

image-20240609144052890

挟持流程:puts –> _IO_puts –> _IO_new_file_xsputn –> _IO_new_file_overflow –> _IO_do_write(FILE, __IO_write_base, size) –> _IO_do_write –> _IO_new_do_write –> new_do_write

当我们能控制__IO_2_1_stdout中的成员变量,就能挟持puts执行_IO_do_write,从而泄露 __IO_write_base 到 __IO_write_ptr之间的数据

绕过

_IO_new_file_overflow –> 设置_flag = 0xFBAD0800

check1:if (f->_flags & _IO_NO_WRITES) ==> 设置_flag标志位不包含_IO_NO_WRITES (8)

check2: if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0 || f->_IO_write_base == NULL) //检查输出缓冲区是否为空 ==> 设置_flag标志位包含_IO_CURRENTLY_PUTTING (0x800)

1
2
3
4
5
6
7
#define _IO_MAGIC 0xFBAD0000
#define _IO_NO_WRITES 8
#define _IO_CURRENTLY_PUTTING 0x800
_flags & _IO_NO_WRITES = 0
_flags = 0xfbad0000
_files & _IO_CURRENTLY_PUTTING = 1
_flags = 0xFBAD0800

new_do_write

我们需要进入以下其中一分支

check1:if (fp->_flags & _IO_IS_APPENDING) ==> 设置_flag标志位包含_IO_IS_APPENDING

check2:if (fp->_IO_read_end != fp->_IO_write_base) ==> 设置_IO_read_end = _IO_write_base就能绕过判断,但是如果我们要进入该分支需要的构造点就有很多了

else if (fp->_IO_read_end != fp->_IO_write_base) //_IO_read_end = _IO_write_base 0x1000的对齐机制,覆盖末位字节难以控制相等
{
off64_t new_pos
= _IO_SYSSEEK (fp, fp->_IO_write_base - fp->_IO_read_end, 1); //调用_IO_SYSSEEK,syseek函数可能会执行不成功而退出
if (new_pos == _IO_pos_BAD)
return 0;
fp->_offset = new_pos;
}

所以我们一般选择进入check1的分支中

1
2
3
4
5
6
7
8
9
#define _IO_MAGIC 0xFBAD0000
#define _IO_NO_WRITES 8
#define _IO_CURRENTLY_PUTTING 0x800
#define _IO_IS_APPENDING 0x1000
_flags = 0xfbad0000
_files & _IO_CURRENTLY_PUTTING = 1
_flags = 0xFBAD0800
_flags & _IO_IS_APPENDING = 1
_flags = 0xfbad1800

再将_IO_write_base设置成为需要泄露的位置,_IO_write_ptr指向泄露结束的地址,即可泄露信息

printf调用链:__printf (printf.c)–> __vfprintf_internal(iovsprintf.c,低版本为vfprintf) –> _IO_file_xsputn –> 后面与puts调用相同

演示

1
2
3
int a = 11111111;
char b[8] = "abcdefg";
printf("%d\n", a);

调用栈示例 2.31-0ubuntu9.7

调用printf前设置_flags字段为 0xfbad1800 ,_IO_write_base为&b,_IO_write_ptr为&b+8

image-20240609154157045

调用栈,这里会打印出 abcdefg

image-20240609154556408

利用 _fileno 字段泄露数据

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
struct _IO_FILE
{
int _flags; /* High-order word is _IO_MAGIC; rest is flags. */

/* The following pointers correspond to the C++ streambuf protocol. */
char *_IO_read_ptr; /* Current read pointer */
char *_IO_read_end; /* End of get area. */
char *_IO_read_base; /* Start of putback+get area. */
char *_IO_write_base; /* Start of put area. */
char *_IO_write_ptr; /* Current put pointer. */
char *_IO_write_end; /* End of put area. */
char *_IO_buf_base; /* Start of reserve area. */
char *_IO_buf_end; /* End of reserve area. */

/* The following fields are used to support backing up and undo. */
char *_IO_save_base; /* Pointer to start of non-current get area. */
char *_IO_backup_base; /* Pointer to first valid character of backup area */
char *_IO_save_end; /* Pointer to end of non-current get area. */

struct _IO_marker *_markers;

struct _IO_FILE *_chain;

int _fileno; //文件描述符
int _flags2;
__off_t _old_offset; /* This used to be _offset but it's too small. */

/* 1+column number of pbase(); 0 is unknown. */
unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[1];

_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};

通过修改 _fileno(_IO_FILE结构偏移0x70处)

进程中系统默认包含三个文件流stdin\stdout\stderr,文件描述符0\1\2。如果修改stdin的_fileno字段为3,即可从文件描述符为3的文件中读取。

演示

1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>

int main(){
char stack_buf[0x100];
FILE *fp = fopen("flag", "rw");
// scanf("%256s", &stack_buf);
// scanf("%256s", &stack_buf);
fgets(stack_buf, sizeof(stack_buf), stdin); //1
fgets(stack_buf, sizeof(stack_buf), stdin); //2
puts(stack_buf);
}

在未执行到1时,stdin还未初始化

image-20240609164506277

执行1以后

image-20240609164539105

此时stdin._fileno被设置为0,执行2将会从标准输入中读取

我们设置stdin._fileno为3,第二个fgets将会从文件描述符为3的文件中读取,即读取flag

image-20240609165814755

stdin任意地址写

任意地址写的实际利用遇到的并不是很多,不写原理了。

绕过

设置 _IO_read_end 等于_IO_read_ptr 。
设置 _flag &~ _IO_NO_READS 即 _flag &~ 0x4。
设置 _fileno 为 0 ,表示读入数据的来源是 stdin 。
设置 _IO_buf_base 为 write_start ,_IO_buf_end 为 write_end ;且使得 _IO_buf_end - _IO_buf_base 大于 fread 要读的数据

stdout任意地址写

绕过

_IO_write_ptr 指向 write_start ,_IO_write_end 指向 write_end 即可实现在目标地址写入数据。 ==> 将打印内容的前size位写入到了write_start中

2.2 虚表

vtable挟持:1.改vtable中的函数指针;2.改vtable指针指向可控内存

vtable一般不可改。

IO 调用的 vtable 函数:

fopen 函数是在分配空间,建立 FILE 结构体,未调用 vtable 中的函数。

fread 函数中调用的 vtable 函数有:

  • _IO_sgetn 函数调用了 vtable 的 _IO_file_xsgetn 。

  • _IO_doallocbuf 函数调用了 vtable 的 _IO_file_doallocate 以初始化输入缓冲区。

  • vtable 中的 _IO_file_doallocate 调用了 vtable 中的 __GI__IO_file_stat 以获取文件信息。

  • __underflow 函数调用了 vtable 中的 _IO_new_file_underflow 实现文件数据读取。

  • vtable 中的 _IO_new_file_underflow 调用了 vtable__GI__IO_file_read 最终去执行系统调用read。

fwrite 函数调用的 vtable 函数有:

  • _IO_fwrite 函数调用了 vtable 的 _IO_new_file_xsputn 。

  • _IO_new_file_xsputn 函数调用了 vtable 中的 _IO_new_file_overflow 实现缓冲区的建立以及刷新缓冲区。

  • vtable 中的 _IO_new_file_overflow 函数调用了 vtable 的 _IO_file_doallocate 以初始化输入缓冲区。

  • vtable 中的 _IO_file_doallocate 调用了 vtable 中的 __GI__IO_file_stat 以获取文件信息。

  • new_do_write 中的 _IO_SYSWRITE 调用了 vtable_IO_new_file_write 最终去执行系统调用write。

fclose 函数调用的 vtable 函数有:

  • 在清空缓冲区的 _IO_do_write 函数中会调用 vtable 中的函数。
  • 关闭文件描述符 _IO_SYSCLOSE 函数为 vtable 中的 __close 函数。
  • _IO_FINISH 函数为 vtable 中的 __finish 函数.

1.fread函数调用_IO_FILE_plus.vtable中的_IO_XSGETN指针
2.fwrite函数调用_IO_FILE_plus.vtable中的_IO_XSPUTN指针,_IO_XSPUTN中会调用同样位于 vtable 中的_IO_OVERFLOW指针
3.fclose函数调用_IO_FILE_plus.vtable中的_IO_FINISH指针
4.printf/puts与fwrite函数调用大致相同,均会调用_IO_XSPUTN指针和_IO_OVERFLOW指针

2.24版本以前可以直接伪造vtable来构造虚假的跳表调用目的函数,2.24以后新增_IO_vtable_check函数

一般都是修改vtable中的指针,然后再触发调用即可实现利用

2.3 __IO_FILE_plus FSOP

<2.24

挟持_IO_list_all指向伪造的__IO_FILE_plus,之后使程序执行_IO_flush_all_lockp函数,刷新_IO_list_all中所有项的文件流,触发_IO_overflow

程序执行 _IO_flush_all_lockp 函数有三种情况:

当 libc 执行 abort 流程时
当执行 exit 函数时
当执行流从 main 函数返回时

挟持方法

1.覆盖 _IO_2_1_stderr_ 结构体

2.利用large bin attack来将_IO_list_all覆盖成为一个chunk地址,然后在chunk上伪造一个fake_IO_FILE结构体

2.4 __IO_str_jumps

2.24~2.28 _IO_vtable_check的增加,限制了伪造vtable指针的区域限制

具体利用可以看对[hctf_2018] the_end的分析

1
2
3
4
5
6
7
8
9
10
11
12
void _IO_vtable_check (void) attribute_hidden;

static inline const struct _IO_jump_t *
IO_validate_vtable (const struct _IO_jump_t *vtable)
{
uintptr_t section_length = __stop___libc_IO_vtables - __start___libc_IO_vtables;
const char *ptr = (const char *) vtable;
uintptr_t offset = ptr - __start___libc_IO_vtables;
if (__glibc_unlikely (offset >= section_length))
_IO_vtable_check ();
return vtable;
}

绕过if (__glibc_unlikely (offset >= section_length)) –> __start___libc_IO_vtables< *vtable < __stop___libc_IO_vtables

_IO_str_jumps 与 __IO_wstr_jumps 就位于 __stop___libc_IO_vtables 和 __start___libc_IO_vtables 之间

将vtable指向 _IO_str_jumps 或者 __IO_wstr_jumps

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
pwndbg> p _IO_str_jumps
$1 = {
__dummy = 0,
__dummy2 = 0,
__finish = 0x7ffff7a83fb0 <_IO_str_finish>,
__overflow = 0x7ffff7a83c90 <__GI__IO_str_overflow>,
__underflow = 0x7ffff7a83c30 <__GI__IO_str_underflow>,
__uflow = 0x7ffff7a82610 <__GI__IO_default_uflow>,
__pbackfail = 0x7ffff7a83f90 <__GI__IO_str_pbackfail>,
__xsputn = 0x7ffff7a82640 <__GI__IO_default_xsputn>,
__xsgetn = 0x7ffff7a82720 <__GI__IO_default_xsgetn>,
__seekoff = 0x7ffff7a840e0 <__GI__IO_str_seekoff>,
__seekpos = 0x7ffff7a82a10 <_IO_default_seekpos>,
__setbuf = 0x7ffff7a82940 <_IO_default_setbuf>,
__sync = 0x7ffff7a82c10 <_IO_default_sync>,
__doallocate = 0x7ffff7a82a30 <__GI__IO_default_doallocate>,
__read = 0x7ffff7a83ae0 <_IO_default_read>,
__write = 0x7ffff7a83af0 <_IO_default_write>,
__seek = 0x7ffff7a83ac0 <_IO_default_seek>,
__close = 0x7ffff7a82c10 <_IO_default_sync>,
__stat = 0x7ffff7a83ad0 <_IO_default_stat>,
__showmanyc = 0x7ffff7a83b00 <_IO_default_showmanyc>,
__imbue = 0x7ffff7a83b10 <_IO_default_imbue>
}
pwndbg> p _IO_file_jumps
$2 = {
__dummy = 0,
__dummy2 = 0,
__finish = 0x7ffff7a809d0 <_IO_new_file_finish>,
__overflow = 0x7ffff7a81740 <_IO_new_file_overflow>,
__underflow = 0x7ffff7a814b0 <_IO_new_file_underflow>,
__uflow = 0x7ffff7a82610 <__GI__IO_default_uflow>,
__pbackfail = 0x7ffff7a83990 <__GI__IO_default_pbackfail>,
__xsputn = 0x7ffff7a801f0 <_IO_new_file_xsputn>,
__xsgetn = 0x7ffff7a7fed0 <__GI__IO_file_xsgetn>,
__seekoff = 0x7ffff7a7f4d0 <_IO_new_file_seekoff>,
__seekpos = 0x7ffff7a82a10 <_IO_default_seekpos>,
__setbuf = 0x7ffff7a7f440 <_IO_new_file_setbuf>,
__sync = 0x7ffff7a7f380 <_IO_new_file_sync>,
__doallocate = 0x7ffff7a74190 <__GI__IO_file_doallocate>,
__read = 0x7ffff7a801b0 <__GI__IO_file_read>,
__write = 0x7ffff7a7fb80 <_IO_new_file_write>,
__seek = 0x7ffff7a7f980 <__GI__IO_file_seek>,
__close = 0x7ffff7a7f350 <__GI__IO_file_close>,
__stat = 0x7ffff7a7fb70 <__GI__IO_file_stat>,
__showmanyc = 0x7ffff7a83b00 <_IO_default_showmanyc>,
__imbue = 0x7ffff7a83b10 <_IO_default_imbue>
}
pwndbg>

image-20240611190216858

io_str_finish利用

通过修改vtable指针指向&_IO_str_jumps-8后,我们成功设置了跳表偏移,我们原本调用__overflow这时会调用__finish

strops.c

1
2
3
4
5
6
7
8
9
void
_IO_str_finish (_IO_FILE *fp, int dummy)
{
if (fp->_IO_buf_base && !(fp->_flags & _IO_USER_BUF))
(((_IO_strfile *) fp)->_s._free_buffer) (fp->_IO_buf_base);
fp->_IO_buf_base = NULL;

_IO_default_finish (fp, 0);
}

调用了_IO_str_finish,我们的目的是调用(((_IO_strfile *) fp)->_s._free_buffer) (fp->_IO_buf_base);

所以需要

  • _IO_buf_base 不为空 可以设置为bin_sh_addr
  • !(fp->_flags & _IO_USER_BUF) #define _IO_USER_BUF 1 ==> _flags低位为0
  • _IO_write_base < _IO_write_ptr
  • _mode <= 0

IO_validate_vtable 利用

2.24以上通过IO_validate_vtable检查vtable指向的地址是否合法

image-20240611194010402

IO_validate_vtable 利用:IO_validate_vtable –> _IO_vtable_check –> rtld_active () –> _dl_addr –> __rtld_lock_unlock_recursive (GL(dl_load_lock))

我们的目标是调用 __rtld_lock_unlock_recursive (GL(dl_load_lock)) 那么需要rtld_active ()返回为true,然后会调用_dl_addr 中的__rtld_lock_unlock_recursive

然后打exit hook类似。

一些话

作为IO_FILE基础入门sky123师傅写的博客是极好的

剩下来与堆利用相结合就不写了

IO利用会跟多的与libc版本相挂钩,也与函数的深层调用有很大关系,通过学习IO_FILE结构能极大的帮助理解函数深层调用的约定

一些关键的libc版本记录:2.24增加_IO_vtable_check;2.28 _IO_str_finish 不再调用 _free_buffer 而是直接是直接调用 free;glibc-2.34 IO_validate_vtable 利用失效;自 glibc-2.27 开始,abort 函数发生较大改动,不再调用 _IO_flush_all_lockp 函数