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的值写入目标地址)