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_FILE
和IO_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, _fileno = 0x2, _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:0000│ 0x7ffff7dd2520 (_IO_list_all) —▸ 0x7ffff7dd2540 (_IO_2_1_stderr_) ◂— 0xfbad2086 pwndbg> telescope 0x7ffff7dd2540 00:0000│ 0x7ffff7dd2540 (_IO_2_1_stderr_) ◂— 0xfbad2086 01:0008│ 0x7ffff7dd2548 (_IO_2_1_stderr_+8) ◂— 0x0 pwndbg> telescope 0x7ffff7dd2620 00:0000│ 0x7ffff7dd2620 (_IO_2_1_stdout_) ◂— 0xfbad2084 01:0008│ 0x7ffff7dd2628 (_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; 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
之间的内容
1
| payload = p64(0xfbad1800)+p64(0)*3+b"\x58"
|
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:0000│ 0x7f7543379980 (_IO_2_1_stdin_+160) —▸ 0x7f75433799c0 (_IO_wide_data_0) ◂— 0x0 01:0008│ 0x7f7543379988 (_IO_2_1_stdin_+168) ◂— 0x0 ... ↓ 2 skipped 04:0020│ 0x7f75433799a0 (_IO_2_1_stdin_+192) ◂— 0xffffffff 05:0028│ 0x7f75433799a8 (_IO_2_1_stdin_+200) ◂— 0x0 06:0030│ 0x7f75433799b0 (_IO_2_1_stdin_+208) ◂— 0x0 07:0038│ 0x7f75433799b8 (_IO_2_1_stdin_+216) —▸ 0x7f75433786e0 (_IO_file_jumps) ◂— 0x0
|
1 2 3 4 5 6 7 8
| pwndbg> telescope 0x7f754337997d+3 00:0000│ 0x7f7543379980 (_IO_2_1_stdin_+160) —▸ 0x7f75433799c0 (_IO_wide_data_0) ◂— 0x0 01:0008│ 0x7f7543379988 (_IO_2_1_stdin_+168) ◂— 0x0 ... ↓ 2 skipped 04:0020│ 0x7f75433799a0 (_IO_2_1_stdin_+192) ◂— 0xffffffff 05:0028│ 0x7f75433799a8 (_IO_2_1_stdin_+200) ◂— 0x0 06:0030│ 0x7f75433799b0 (_IO_2_1_stdin_+208) ◂— 0x0 07:0038│ 0x7f75433799b8 (_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>, __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:完全失效