_IO_FILE基础利用
_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结构关系图
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结构体中的成员变量来达成任意地址读的目的。
挟持流程: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 |
|
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 |
|
再将_IO_write_base设置成为需要泄露的位置,_IO_write_ptr指向泄露结束的地址,即可泄露信息
printf调用链:__printf (printf.c)–> __vfprintf_internal(iovsprintf.c,低版本为vfprintf) –> _IO_file_xsputn –> 后面与puts调用相同
演示
1 | int a = 11111111; |
调用栈示例 2.31-0ubuntu9.7
调用printf前设置_flags字段为 0xfbad1800 ,_IO_write_base为&b,_IO_write_ptr为&b+8
调用栈,这里会打印出 abcdefg
利用 _fileno 字段泄露数据
1 | struct _IO_FILE |
通过修改 _fileno(_IO_FILE结构偏移0x70处)
进程中系统默认包含三个文件流stdin\stdout\stderr,文件描述符0\1\2。如果修改stdin的_fileno字段为3,即可从文件描述符为3的文件中读取。
演示
1 |
|
在未执行到1时,stdin还未初始化
执行1以后
此时stdin._fileno被设置为0,执行2将会从标准输入中读取
我们设置stdin._fileno为3,第二个fgets将会从文件描述符为3的文件中读取,即读取flag
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 | void _IO_vtable_check (void) attribute_hidden; |
绕过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 | pwndbg> p _IO_str_jumps |
io_str_finish利用
通过修改vtable指针指向&_IO_str_jumps-8后,我们成功设置了跳表偏移,我们原本调用__overflow这时会调用__finish
strops.c
1 | void |
调用了_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指向的地址是否合法
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
函数