io file 结构
struct _IO_FILE { |
struct _IO_FILE *_chain _链接file结构形成一个链表,IO_list_all全局变量表示链表的头部
初始有stdin stdout stderr 但是这三个文件流在libc中
_IO_2_1_stderr_ |
这三个指针是libc中 指向FILE结构
_IO_FILE结构外包裹着IO_FILE_plus 包含vtable 指向一系列函数指针
struct _IO_FILE_plus |
在64位系统下 vtable 偏移为0xd8
IO_jump_t 中保存了一些函数的指针
void * funcs[] = { |
fread
size_t fread ( void *buffer, size_t size, size_t count, FILE *stream) ; |
count 是记录的个数
返回读取到的记录个数
_IO_size_t |
这个函数是其内部真正实现功能的函数
_IO_XSGETN 是_IO_FILE_plus.vtable中的函数指针 。
默认指向_IO_file_xsgetn
fwrite
主要是调用_IO_XSPUTN 来实现写入的功能
_IO_XSPUTN位于vtable中 默认对应_IO_new_file_xsputn 会调用_IO_OVERFLOW
_IO_OVERFLOW默认对应_IO_new_file_overflow
fopen 用于打开文件
fopen返回一个文件指针 内部会创建FILE结构,进行一些初始化操作
*new_f = (struct locked_FILE *) malloc (sizeof (struct locked_FILE)); |
先为FILE分配空间. 然后初始化vtable 调用IO_file_init进一步初始化
_IO_JUMPS (&new_f->fp) = &_IO_file_jumps; |
初始化过程中调用_IO_Link_in 把新分配的FILE 链入链表 并且是插入头部
之后__fopen_internal调用_IO_file_fopen 打开目标文件 。
fclose: 可以把缓冲区内最后剩余的数据输出到磁盘 释放文件指针和缓冲区
flose 首先调用_IO_unlink_it 将指定file脱链
之后调用_IO_file_close_it 会调用系统接口close关闭文件
最后调用vtable 的_IO_FINISH 对应_IO_file_finish ,函数中用到free释放FILE结构
printf/puts
printf 参数如果是\n结束的纯字符串 会被优化成puts
puts 用_IO_puts 实现的 这个函数操作与 fwrite大致相同
printf
:
vfprintf+11
_IO_file_xsputn
_IO_file_overflow
funlockfile
_IO_file_write
write
伪造 vtable 劫持程序流程
核心思想是篡改vtable 把vatle指向控制的内存 在里面布置函数指针
一共分为两种 一种是直接改写vtable里的指针 另一个是覆盖vtable的指针指向能控制的内存
实例:
首先要知道_IO_FILE_plus的位置
int main(void) |
根据偏移得到vtable地址 64位是0xd8,然后搞清楚劫持的IO函数调用了vtable的哪个函数,printf调用xsputsm
传入的第一个参数应该是对应的_IO_FILE_plus地址,因此还可以控制参数
|
在目前的libc2.23 位于libc段的vtable不可以写入。可以通过在内存中伪造vtable利用
|
可以选择libc 中的IO_FILE 在libc2.23之前这些vtable可以写入
FSOP
File Stream Oriented Programming
核心是劫持_IO_list_all的值来伪造链表和其中的_IO_FILE
FSOP 选择的触发方法是调用_IO_flush_all_lockp,会刷新链表中的所有文件流
相当于对每个FILE调用fflush 对应调用_IO_FILE_plus.vtable 中的_IO_overflow
当glibc检测到memory corruption时
会有以下流程
malloc_printerr -> _libc_message ->abort -> _IO_flush_all_lockp ->JUMP_field
- libc 执行abort流程时
- 执行exit函数时
- 执行流从main函数返回时
会运行
_IO_flush_all_lockp
FSOP首先需要知道libc地址 _IO_list_all存储在libc中
然后通过任意地址写把_IO_list_all内容改为指向可控内存的指针
为了能让fake_FILE正常工作 需要让fp-_mode = 0
fp->_IO_write_ptr > fp->_IO_write_base
使用分配内存的前0x100作为_IO_FILE
后0x100个字节作为vtable
然后覆盖_IO_list_all 让它指向伪造的 _IO_FILE_plus
调用exit 程序就会执行_IO_flush_all_lockp 然后就可以调用其中的_IO_overflow
glibc 2.24中 会在调用虚函数之前检查vtable地址是否在_IO_vtable中
如果不在的话会调用_IO_vtable_check进一步检查
新的利用技术 关注点转移到了_IO_FILE结构内部的域 控制这些数据实现任意写或者任意读
_IO_FILE中_IO_buf_base表示操作起始地址 _IO_buf_end表示结束地址
例如scanf,stdin的初始化内存是在堆上分配的,
分配大小是0x400 对应 _IO_buf_base - _IO_buf_end
尝试修改_IO_buf_base可以实现任意地址读写
_IO_str_jumps 的vtable不在check范围内,vtable修改成_IO_str_jumps 就能调用不一样的文件操作函数
- fp->_flags & _IO_NO_WRITES为假
- (pos = fp->_IO_write_ptr - fp->_IO_write_base) >= ((fp->_IO_buf_end - fp->_IO_buf_base) + flush_only(1))
- fp->_flags & _IO_USER_BUF(0x01)为假
- 2*(fp->_IO_buf_end - fp->_IO_buf_base) + 100 不能为负数
- new_size = 2 * (fp->_IO_buf_end - fp->_IO_buf_base) + 100; 应当指向/bin/sh字符串对应的地址
- fp+0xe0指向system地址
或者用finish
_flags = (binsh_in_libc + 0x10) & ~1 |