0%

IO_FILE pwn

IO_FILE pwn

FILE 在 Linux 系统的标准 IO 库中是用于描述文件的结构,称为文件流

FILE 结构在程序执行 fopen 等函数时会进行创建,并分配在堆中,然后以链表的形式串联起来,但系统一开始会自动创建的三个文件即 stdin、stdout、stderr,它们是在libc上

先借 libc-2.23 说明一下 FILE 结构:

在 libc-2.23 版本中,有个全局变量_IO_list_all,该变量指向了FILE链表的头部

首先认识一个结构体_IO_FILE_plus,所有 FILE 文件结构都是这样的一个结构体,整个结构体如下代码所示,其中又包括了两个重要的结构体_IO_FILEIO_jump_t

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

在 GDB 中输入以下命令可以打印 _IO_list_all :(这里通过在前面加上结构体类型可以详细的打印其内存数据信息)

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
pwndbg> p/x*(struct _IO_FILE_plus*)_IO_list_all
$1 = {
file = {
_flags = 0xfbad2086,
_IO_read_ptr = 0x0,
_IO_read_end = 0x0,
_IO_read_base = 0x0,
_IO_write_base = 0x0,
_IO_write_ptr = 0x0,
_IO_write_end = 0x0,
_IO_buf_base = 0x0,
_IO_buf_end = 0x0,
_IO_save_base = 0x0,
_IO_backup_base = 0x0,
_IO_save_end = 0x0,
_markers = 0x0,
_chain = 0x7ffff7dd2620, // 指向下一个链表节点(stderr的下一个:stdout)
_fileno = 0x2, // fileno值为2(标准错误流的文件描述符就是'2')
_flags2 = 0x0,
_old_offset = 0xffffffffffffffff,
_cur_column = 0x0,
_vtable_offset = 0x0,
_shortbuf = {0x0},
_lock = 0x7ffff7dd3770,
_offset = 0xffffffffffffffff,
_codecvt = 0x0,
_wide_data = 0x7ffff7dd1660,
_freeres_list = 0x0,
_freeres_buf = 0x0,
__pad5 = 0x0,
_mode = 0x0,
_unused2 = {0x0 <repeats 20 times>}
},
vtable = 0x7ffff7dd06e0
}

这就是 _IO_list_all 变量, 其指向 FILE 链表的头部 ,结合上面的结构体可知,file对应的就是 _IO_FILE 结构类型,vtable 对应的就是 _IO_jump_t 类型

在没有创建其它文件结构时, _IO_list_all 指向stderr,然后依次是 stdout 和 stdin(可以通过打印 _chain 进行查看):

1
2
3
4
5
6
7
8
9
pwndbg> p/x _IO_list_all
$2 = 0x7ffff7dd2540
pwndbg> p/x stderr
$3 = 0x7ffff7dd2540

pwndbg> p/x _IO_list_all.file._chain
$4 = 0x7ffff7dd2620
pwndbg> p/x stdout
$5 = 0x7ffff7dd2620
1
2
3
4
5
6
7
8
pwndbg> telescope 0x7ffff7dd2520
00:00000x7ffff7dd2520 (_IO_list_all) —▸ 0x7ffff7dd2540 (_IO_2_1_stderr_) ◂— 0xfbad2086
pwndbg> telescope 0x7ffff7dd2540
00:00000x7ffff7dd2540 (_IO_2_1_stderr_) ◂— 0xfbad2086
01:00080x7ffff7dd2548 (_IO_2_1_stderr_+8) ◂— 0x0
pwndbg> telescope 0x7ffff7dd2620
00:00000x7ffff7dd2620 (_IO_2_1_stdout_) ◂— 0xfbad2084
01:00080x7ffff7dd2628 (_IO_2_1_stdout_+8) ◂— 0x0

由于 _IO_FILE_plus 中只是存储了 vtable 的指针,并没有存储详细的结构信息,所以这里我们进一步打印一下 vtable :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
pwndbg> p*(struct _IO_jump_t*)_IO_list_all.vtable 
$6 = {
__dummy = 0,
__dummy2 = 0,
__finish = 0x7ffff7a869d0 <_IO_new_file_finish>,
__overflow = 0x7ffff7a87740 <_IO_new_file_overflow>,
__underflow = 0x7ffff7a874b0 <_IO_new_file_underflow>,
__uflow = 0x7ffff7a88610 <__GI__IO_default_uflow>,
__pbackfail = 0x7ffff7a89990 <__GI__IO_default_pbackfail>,
__xsputn = 0x7ffff7a861f0 <_IO_new_file_xsputn>,
__xsgetn = 0x7ffff7a85ed0 <__GI__IO_file_xsgetn>,
__seekoff = 0x7ffff7a854d0 <_IO_new_file_seekoff>,
__seekpos = 0x7ffff7a88a10 <_IO_default_seekpos>,
__setbuf = 0x7ffff7a85440 <_IO_new_file_setbuf>,
__sync = 0x7ffff7a85380 <_IO_new_file_sync>,
__doallocate = 0x7ffff7a7a190 <__GI__IO_file_doallocate>,
__read = 0x7ffff7a861b0 <__GI__IO_file_read>,
__write = 0x7ffff7a85b80 <_IO_new_file_write>,
__seek = 0x7ffff7a85980 <__GI__IO_file_seek>,
__close = 0x7ffff7a85350 <__GI__IO_file_close>,
__stat = 0x7ffff7a85b70 <__GI__IO_file_stat>,
__showmanyc = 0x7ffff7a89b00 <_IO_default_showmanyc>,
__imbue = 0x7ffff7a89b10 <_IO_default_imbue>
}

fileno 劫持

这个利用比较局限,一般在有“open”的程序中使用

利用的原理很简单,通过上面的分析,我们可以看到 fileno 的值就是文件描述符,位于 stdin 文件结构开头0x70偏移处,在漏洞利用中可以通过 修改 stdin 的 fileno 值来重定位需要读取的文件 (原本是“0”,从标准输入中读取,现在改为“fd”,从目标文件中读取),接下来类似于 read 之类的函数都会从目标文件中进行读取(修改为“flag”的描述符,就可以读取“flag”)

IO_2_1_stdout leak

可以在没有打印模块的情况下 leak libc_base,通常与 house of roman 结合

这个攻击需要用到结构体 _IO_FILE

1
2
3
4
5
6
7
8
9
10
11
12
struct _IO_FILE {
int _flags;
/* 文件标志,简单的说:像puts一类的输入输出函数要想正确的打印信息就需要正确设置该字段 */
char* _IO_read_ptr; /* 始终指向缓冲区中已被用户读走的字符的下一个 */
char* _IO_read_end; /* 指向"读缓冲区"的末尾 */
char* _IO_read_base; /* 指向"读缓冲区" */
char* _IO_write_base; /* 指向"写缓冲区" */
char* _IO_write_ptr; /* 始终指向缓冲区中已被用户写入的字符的下一个 */
char* _IO_write_end; /* 指向"写缓冲区"的末尾*/
......
......
}

我们一般将堆块分配到 stdout 指针处存储的 _IO_2_1_stdout_ 处:(这里是一个 IO_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
pwndbg> p stdout 
$1 = (struct _IO_FILE *) 0x7ffff7dd0760 <_IO_2_1_stdout_>
pwndbg> ptype stdout
type = struct _IO_FILE {
int _flags;
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;
int _fileno;
int _flags2;
__off_t _old_offset;
unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[1];
_IO_lock_t *_lock;
__off64_t _offset;
struct _IO_codecvt *_codecvt;
struct _IO_wide_data *_wide_data;
struct _IO_FILE *_freeres_list;
void *_freeres_buf;
size_t __pad5;
int _mode;
char _unused2[20];
} *

修改其 _flags 为 “0xfbad1800”(或者“0xfbad3887”),将后面三个read指针置空,将 _IO_write_base 处的第一个字节改为0x58(或者“0”),后面的 _IO_write_ptr_IO_write_end 保持不变(该flags这样设置只是针对puts函数,其余打印函数略有不同)

当程序遇到puts函数时就会打印 _IO_write_base_IO_write_ptr 之间的内容

  • 泄露 _IO_file_jumps 的写法:
1
payload = p64(0xfbad1800)+p64(0)*3+b"\x58"
  • 泄露 _IO_2_1_stdin_ 的写法:
1
payload = p64(0xfbad3887)+p64(0)*3+p8(0)

vtable 劫持

如果程序开了 Full RELRO ,禁用了 hook ,并且没有法控制栈,这时就要考虑 vtable 劫持了

vtable是 _IO_FILE_plus 结构体里的一个字段,是一个函数表指针,里面存储着许多和 IO 相关的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
pwndbg> p*(struct _IO_jump_t*)_IO_list_all.vtable
$1 = {
__dummy = 0,
__dummy2 = 0,
__finish = 0x7ffff7e68510 <_IO_new_file_finish>,
__overflow = 0x7ffff7e69320 <_IO_new_file_overflow>,
__underflow = 0x7ffff7e68fc0 <_IO_new_file_underflow>,
__uflow = 0x7ffff7e6a4c0 <__GI__IO_default_uflow>,
__pbackfail = 0x7ffff7e6bc90 <__GI__IO_default_pbackfail>,
__xsputn = 0x7ffff7e67b20 <_IO_new_file_xsputn>,
__xsgetn = 0x7ffff7e677a0 <__GI__IO_file_xsgetn>,
__seekoff = 0x7ffff7e66d90 <_IO_new_file_seekoff>,
__seekpos = 0x7ffff7e6ac30 <_IO_default_seekpos>,
__setbuf = 0x7ffff7e66a60 <_IO_new_file_setbuf>,
__sync = 0x7ffff7e668f0 <_IO_new_file_sync>,
__doallocate = 0x7ffff7e5a600 <__GI__IO_file_doallocate>,
__read = 0x7ffff7e67e50 <__GI__IO_file_read>,
__write = 0x7ffff7e67390 <_IO_new_file_write>,
__seek = 0x7ffff7e66b30 <__GI__IO_file_seek>,
__close = 0x7ffff7e66a50 <__GI__IO_file_close>,
__stat = 0x7ffff7e67370 <__GI__IO_file_stat>,
__showmanyc = 0x7ffff7e6be20 <_IO_default_showmanyc>,
__imbue = 0x7ffff7e6be30 <_IO_default_imbue>
}

这个函数表中有19个函数,分别完成IO相关的功能,由IO函数调用,如 fwrite 最终会调用 __write 函数、 fread 会调用 __doallocate 来分配IO缓冲区等

vtable劫持的原理是:如果能够控制 FILE 结构体(一般是控制“stdin”的 _IO_FILE_plus 结构体),实现对 vtable 指针的修改,使得 vtable 指向可控的内存(一般在这片内存的各个区域中,全部写入“one_gadget”),在该内存中构造好 vtable,再通过调用相应IO函数,触发 vtable 函数的调用,即可劫持程序执行流(劫持最关键的点在于修改 _IO_FILE_plus 结构体的 vtable 指针)

1
2
3
4
5
6
7
8
pwndbg> telescope 0x7f754337997d+3
00:00000x7f7543379980 (_IO_2_1_stdin_+160) —▸ 0x7f75433799c0 (_IO_wide_data_0) ◂— 0x0
01:00080x7f7543379988 (_IO_2_1_stdin_+168) ◂— 0x0
... ↓ 2 skipped
04:00200x7f75433799a0 (_IO_2_1_stdin_+192) ◂— 0xffffffff
05:00280x7f75433799a8 (_IO_2_1_stdin_+200) ◂— 0x0
06:00300x7f75433799b0 (_IO_2_1_stdin_+208) ◂— 0x0
07:00380x7f75433799b8 (_IO_2_1_stdin_+216) —▸ 0x7f75433786e0 (_IO_file_jumps) ◂— 0x0 // vtable的位置
1
2
3
4
5
6
7
8
pwndbg> telescope 0x7f754337997d+3
00:00000x7f7543379980 (_IO_2_1_stdin_+160) —▸ 0x7f75433799c0 (_IO_wide_data_0) ◂— 0x0
01:00080x7f7543379988 (_IO_2_1_stdin_+168) ◂— 0x0
... ↓ 2 skipped
04:00200x7f75433799a0 (_IO_2_1_stdin_+192) ◂— 0xffffffff
05:00280x7f75433799a8 (_IO_2_1_stdin_+200) ◂— 0x0
06:00300x7f75433799b0 (_IO_2_1_stdin_+208) ◂— 0x0
07:00380x7f75433799b8 (_IO_2_1_stdin_+216) —▸ 0x562989a57010 ◂— 0x0 // 成功修改

​ // 本文是基于 libc-2.23 及之前的libc上可实施的,libc-2.24 之后加入了 vtable check 机制,无法再构造vtable

FSOP

FSOP( File Stream Oriented Programming ),是一种劫持 _IO_list_all 来伪造文件流对象链表的利用技术,常与 house of orange 连用,通过调用 _IO_flush_all_lockp 函数触发

该函数会在下面三种情况下被调用:

  • 第一,libc 检测到内存错误从而执行 abort 流程时
  • 第二,执行 exit 函数时
  • 第三,main 函数返回时

先分析 _IO_flush_all_lockp 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int _IO_flush_all_lockp ()
{
int result = 0;
struct _IO_FILE *fp;
for (fp = (_IO_FILE *) _IO_list_all; fp; fp = fp->_chain)
if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)
#if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
|| (fp->_vtable_offset == 0
&& fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
> fp->_wide_data->_IO_write_base))
#endif
)
&& _IO_OVERFLOW (fp, EOF) == EOF)
result = EOF;
return result;
}

在执行 _IO_OVERFLOW 前有两个检查:(通常是采用这个伪造)

  • _mode >= 0
  • _IO_write_base < _IO_write_ptr

而对 _IO_OVERFLOW 的调用是 虚表调用 ,是可以进行劫持的:

1
2
3
4
5
6
7
8
9
10
11
pwndbg> p*(struct _IO_jump_t*)_IO_list_all.vtable
$1 = {
__dummy = 0,
__dummy2 = 0,
__finish = 0x7ffff7e68510 <_IO_new_file_finish>,
__overflow = 0x7ffff7e69320 <_IO_new_file_overflow>, // target
__underflow = 0x7ffff7e68fc0 <_IO_new_file_underflow>,
................
__showmanyc = 0x7ffff7a89b00 <_IO_default_showmanyc>,
__imbue = 0x7ffff7a89b10 <_IO_default_imbue>
}

可见第4片区域就是 __overflow 的虚表,我们可以劫持该虚表为“one_gadget”或者其他需要的函数

利用姿势

一般在pwn题中,我们都是构造内存错误,此时会产生一系列的函数调用路径,最终的调用为:_IO_flush_all_lockp —> _IO_OVERFLOW,而这里的 _IO_OVERFLOW 就是文件流对象虚表的第四项指向的内容 _IO_new_file_overflow

因此我们的利用思路一般是:

  • 要么直接修改文件流对象
  • 要么伪造一个_IO_FILE结构体

版本影响

libc-2.24:多了 vtable check,这就意味着我们不能利用任意地址来充当vtable

libc-2.27:完全失效