IO_FILE相关笔记
IO_FILE相关笔记
一、IO_FILE相关结构
_IO_FILE_plus、_IO_FILE、_IO_jump_t结构体的定义分别为:
1 | |
进程中FILE结构通过_chain域构成一个链表,链表头部为_IO_list_all全局变量,默认情况下依次链接了stderr,stdout,stdin三个文件流,并将新建的流插入到头部,vtable虚表为_IO_file_jumps。

此外,还有_IO_wide_data结构体:
1 | |
宏的定义
1 | |
IO 调用的 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函数。
正常情况下exit函数调用的函数链:
exit()–>__run_exit_handlers()–>exit_function_list -> _dl_fini()–>__dl_rtld_lock_recursive和__dl_rtld_unlock_recursive
IO_validate_vtable 函数的调用链:(在调用vtable前执行,且rtld_active为true时)
IO_validate_vtable–>rtld_active–>_dl_addr–>__rtld_lock_lock_recursive (GL(dl_load_lock))
二、IO_FILE attack 之 FSOP (libc 2.23 & 2.24)
主要原理为劫持vtable与_chain,伪造IO_FILE,主要利用方式为调用**IO_flush_all_lockp()**函数触发。IO_flush_all_lockp()函数将在以下三种情况下被调用:
libc检测到内存错误,从而执行abort函数时(在glibc-2.26删除)。- 程序执行
exit函数时。 - 程序从
main函数返回时。
源码:
1 | |

当满足以下条件,就会执行_IO_OVERFLOW()
1 | |
所以在2.23的时候就可以这样构造来打FSOP
1 | |
这时候chunk_addr里面的 _IO_write_ptr > _IO_write_base且base=0,就会触发chunk_addr的 _IO_OVERFLOW函数,执行 _IO_OVERFLOW(fp),也就是system(‘/bin/sh\x00’)
而libc-2.24加入了对虚表的检查IO_validate_vtable()与IO_vtable_check(),若无法通过检查,则会报错:Fatal error: glibc detected an invalid stdio handle。
1 | |
1 | |
glibc中有一段完整的内存存放着各个vtable,其中__start___libc_IO_vtables指向第一个vtable地址_IO_helper_jumps,而__stop___libc_IO_vtables指向最后一个vtable_IO_str_chk_jumps结束的地址。
若指针不在glibc的vtable段,会调用_IO_vtable_check()做进一步检查,以判断程序是否使用了外部合法的vtable(重构或是动态链接库中的vtable),如果不是则报错。
具体源码如下:
1 | |
因此,最好的办法是:我们伪造的vtable在glibc的vtable段中,从而得以绕过该检查。
目前来说,有四种思路:利用_IO_str_jumps中_IO_str_overflow()函数,利用_IO_str_jumps中_IO_str_finish()函数与利用_IO_wstr_jumps中对应的这两种函数,先来介绍最为方便的:
利用_IO_str_jumps中_IO_str_overflow()函数的手段。(2.28)
_IO_str_jumps的结构体如下:
1 | |
其中,_IO_str_finish源代码如下:
1 | |
其中相关的_IO_str_fields结构体与_IO_strfile_结构体的定义:
1 | |
可以看到,它使用了IO结构体中的值当作函数地址来直接调用,如果满足条件,将直接将fp->_s._free_buffer当作函数指针来调用。
首先,仍然需要绕过之前的_IO_flush_all_lokcp函数中的输出缓冲区的检查_mode<=0以及_IO_write_ptr>_IO_write_base进入到_IO_OVERFLOW中。(这里_IO_overflow在下一次malloc中触发,把_IO_overflow修改为system,_s._free_buffer修改为/bin/sh的地址;或者把_IO_overflow修改成one_gadget)
利用_IO_str_jumps中_IO_str_finish()函数的手段。(2.28)
我们可以将vtable的地址覆盖成_IO_str_jumps-8,这样会使得_IO_str_finish函数成为了伪造的vtable地址的_IO_OVERFLOW函数(因为_IO_str_finish偏移为_IO_str_jumps中0x10,而_IO_OVERFLOW为0x18)。这个vtable(地址为_IO_str_jumps-8)可以绕过检查,因为它在vtable的地址段中。
构造好vtable之后,需要做的就是构造IO FILE结构体其他字段,以进入将fp->_s._free_buffer当作函数指针的调用:先构造fp->_IO_buf_base为/bin/sh的地址,然后构造fp->_flags不包含_IO_USER_BUF,它的定义为#define _IO_USER_BUF 1,即fp->_flags最低位为0。
最后构造fp->_s._free_buffer为system_addr或one gadget即可getshell。
由于libc中没有_IO_str_jump的符号,因此可以通过_IO_str_jumps是vtable中的倒数第二个表,用vtable的最后地址减去0x168定位
总结:修改 ((_IO_strfile *) fp)->_s._free_buffer 为 system 地址,然后修改 fp->_IO_buf_base 为 /bin/sh 字符串地址,然后触发程序执行 _IO_str_finish 函数就可以得到 shell 。
house of orange(2.23)

当进行unsortedbins_attack时,这里的 _IO_list_all 的值会被修改为top,因为IO_list_all包含IO_FILE和IO_str_jumps两个结构体,然后此时smallbin[4]的fd字段,这时候这个字段就会对应 ._chain的值——就是说,此时smallbins[4]的fd字段对应着_IO_FILE这个结构体,同时里面是被我们放入smallbin的topchunk,这时候只要布置topchunk就是在伪造IO_FILE(然后接)
stdin劫持实现任意地址写
scanf,fread,gets等读入走IO指针(read不走)。
_IO_2_1_stdin结构体
1 | |
一般设置flag位为0xfbad2887
(1) 设置_IO_read_end等于_IO_read_ptr(使得输入缓冲区内没有剩余数据,从而可以从用户读入数据)。
(2) 设置_flag &~ _IO_NO_READS即_flag &~ 0x4(一般不用特意设置)。
(3) 设置_fileno为0(一般不用特意设置)。
(4) 设置_IO_buf_base为write_start,_IO_buf_end为write_end(我们目标写的起始地址是write_start,写结束地址为write_end),且使得_IO_buf_end-_IO_buf_base大于要写入的数据长度。
stdout 任意地址读写
1 | |
printf,fwrite,puts等输出走IO指针(write不走)。
读
(1) 设置_flag &~ _IO_NO_WRITES,即_flag &~ 0x8;
(2) 设置_flag & _IO_CURRENTLY_PUTTING,即_flag | 0x800;
(3) 设置_fileno为1;
(4) 设置_IO_write_base指向想要泄露的地方,_IO_write_ptr指向泄露结束的地址;
(5) 设置_IO_read_end等于_IO_write_base 或 设置_flag & _IO_IS_APPENDING即,_flag | 0x1000。
此外,有一个大前提:需要调用_IO_OVERFLOW()才行,因此需使得需要输出的内容中含有\n换行符 或 设置_IO_write_end等于_IO_write_ptr(输出缓冲区无剩余空间)等。
一般来说,经常利用puts函数加上述stdout任意读的方式泄露libc。
比如:泄露 libc 基址
对于没有输出功能的堆题,要想泄露 libc 基址就需要劫持_IO_2_1_stdout_ 结构体,在libc-2.23 版本可以利用 fast bin attack 在_IO_2_1_stdout_-0x43处申请 fast bin。然后把write_base最后一位改小,下一次输出就会先输出write_buf
写
全缓冲
(1)设置flag位(0xfbad1800)
(2)设置_IO_write_end指向write_end,_IO_write_ptr指向write_start;
(3)设置其他为0
写的内容是输出函数的参数(比如printf(%s,&a)就会把a的值写入目标地址)