IO_FILE总结
FILE结构
当我们用fopen()
打开一个文件时,glibc会创建名为_IO_FILE_plus
的结构体,结构如下。
由于文件的操作都一样,因此每个结构体里都会放一个指向_IO_jump_t的结构体, 类似于C++中的虚函数表 ,fread(), fwrite(), fclose()
本质上都在调用这个表里的函数(值得注意的是,puts是通过_IO_puts
来实现的,printf在输出带换行符的纯字符串时会被优化为puts)。调用时,会通过IO_FILE指针+sizeof(FILE)+vtable_offset
来定位vatble,因此vtable_offset
默认是0。
进程中的 FILE 结构会通过_chain
域彼此连接形成一个链表,链表头部用全局变量_IO_list_all
表示,通过这个值我们可以遍历所有的 FILE 结构。有趣的是,在fclose时,会调用unlink来脱链,如果篡改 _chain形成闭环,就会造成死循环。
在标准 I/O 库中,每个程序启动时有三个文件流是自动打开的:stdin、stdout、stderr。当新的文件结构被创建,_IO_list_all
指向新的文件,新的文件的_chain指向stderr。
调试可知,通过fopen打开的FILE在堆上,stderr等则在libc段上。
虚表劫持
1 | printf/puts 最终会调用 _IO_FILE_xsputn |
2.24以前,劫持虚表很简单,大概就是以下两种办法,无非就是让vatble指向fake_vtable。
- 劫持文件指针:fp->fake_file->fake_vtable
- 劫持vtable指针:real_file.vtable->fake_vtable
用一个简单的demo可以演示一下
1 |
|
可以做一下pwnable.tw的seethefile来熟悉以下,具体思路是通过 /proc/self/maps
来leak,然后用第一种办法来getshell。(写的详解丢了,但是这题很简单,应该不会太为难你们吧嘻嘻
FSOP
如果没有可控的FILE可以进行fclose这些操作,那我们可以用FSOP。
FSOP全称是File Stream Oriented Programming
,关键点在于_IO_list_all
指针,通过调用 _IO_flush_all_lockp()
函数来触发,该函数会在下面三种情况下被调用:
- libc 检测到内存错误时
- 执行 exit 函数时
- main 函数返回时
当 glibc 检测到内存错误时,会依次调用这样的函数路径:
malloc_printerr -> __libc_message -> __GI_abort -> _IO_flush_all_lockp -> _IO_OVERFLOW
。
1 | int |
也就是会调用到vatble,只要链上有一个FILE可控,我们就能劫持程序流。
条件
FSOP 利用的条件:
- 首先需要攻击者获知 libc基址,因为
_IO_list_all
是作为全局变量储存在 libc.so 中的,不泄漏 libc 基址就不能改写_IO_list_all
。 - 之后需要用任意地址写把
_IO_list_all
的内容改为指向我们可控内存的指针,或者将某个IO FILE的_chain 指向我们的可控内存就行。 - 布置数据
House of Orange
主要适用于没有free功能的情况(libc<2.24)
需要leak libc和heap
原理
Top chunk的大小小于请求值的情况下有两种可能性。
1) 延长Top chunk
2) mmap一个新的页
如果请求的大小小于0x21000,那么就按照前者。这会强制调用 sysmalloc。而sysmalloc中,只要满足一定条件,就会调用_int_free
,将old_top放入UB之中。
修改top_chunk size
sysmalloc中对于old top chunk size有验证
1 | assert ((old_top == initial_top (av) && old_size == 0) || |
- 大于MINSIZE(0X10)
- 小于所需的大小 + MINSIZE
- prev inuse位设置为1
- old_end的值是页对齐的(也就old_top+old_size是整0x1000的)
例如这里,我们申请了0x400大小的堆块,此时old_top是0x602400,为了使得修修改的size符合要求,我们可以设置成0xc01,此时0x602400 + 0xc00 == 0x603000
free top chunk
此时申请0x1000大小的chunk,就满足了4个条件,原top_chunk被放入UB之中。(不要大于0x21000),新的top_chunk则变成了0x624010,也就是0x603000 + 0x10 + 0x21000。中间一大块地方未被使用。
由于_IO_list_all
和main_arena
都在libc段,我们可以计算得到_IO_list_all
。
)
我们的想法是用一个假的文件指针覆盖_IO_list_all
指针,其_IO_OVERLOW
指向system,其前8个字节被设置为’/bin/sh’,这样,调用_IO_OVERFLOW(fp, EOF)就转化为system(‘/bin/sh’)。
当然,如果是普通的CTF我还是喜欢one gadget
UB attack control _IO_list_all
通过 Unsorted Bin Attack | Brvc3’s Base ,我们就可以将_IO_list_all
改成main_arena + 88
。
main_arena
不能完全被控制,要靠chain字段来转移到下一个_IO_FILE
来方便利用。_chain
字段的偏移为0x68,所以要将(main_arena+88)+0x68=(main_arena+192)的位置覆盖成top的地址,这样就会把top当成下一个_IO_FILE
,而top又是我们可控的地方,在top里伪造虚表,并覆盖伪造虚表里的overflow函数地址为system地址。
main_arena+192实际上就是smallbin-4它保存了所有大小在90到98之间的small bin。我们只要再将top的size改成0x61(97),然后申请一个比它小的chunk,它就会被放到small bin - 4之中。覆盖也就完成了。
fake file
为了执行_IO_OVERFLOW,需要满足之前的判断:
1 | - fp->_mode <= 0不成立,所以fp->_mode > 0 |
按这些成员的偏移设置即可。
1 |
|
实际上,pwnlib集成了house of orange的构造,然而我个人感觉不好用
1 | 'amd64') context.clear(arch= |
例题
pwnable的bookwriter
逆向分析
菜单题,增查改
author_name, page_list和size_list在bss段上连续,由于输入函数只会把\n换成截止符,所以info可以leak出page_list[0]。
page_list和size_list的长度都是8。add对于page数量操作不当导致了溢出,size_list[0]可以被地址覆盖,进而搭配edit造成堆溢出。
edit会重置size,配合输入函数的洞也可以越界写。
hijack top chunk size
当我们malloc(0x18),并输入0x18个字符,此时这些字符与top_chunk的size相连
edit()之后,size就会大于0x18,从而可以控制top_chunk的size
1 | rct('Author :') |
leak heap
1 | # leak heap |
这一步有一点点看概率,如果堆地址是0010结尾的,那就会被截断而得不到堆地址。
而且要注意,info用到了scanf,会申请一个堆块,因此这一步最好不要放在前面。
UB attack
1 | # free top chunk |
house of orange
1 | # house of orange |
是的,Pwnlib自带的FILE攻击模块用起来像一坨屎,不如自己写一个。
2.24保护措施
libc-2.24 中加入了对 vtable 指针的检查。这个 commit 新增了两个函数:IO_validate_vtable
和 _IO_vtable_check
。所有的 libio vtables 被放进了专用的只读的 __libc_IO_vtables
段,以使它们在内存中连续。在任何间接跳转之前,vtable 指针将根据段边界进行检查,如果指针不在这个段,则调用函数 _IO_vtable_check()
做进一步的检查,并且在必要时终止进程。
Bypass
既然vtable加了检查,那我们可以尝试利用其他的成员。事实上我们是可以达成:
- 任意地址读
- 任意地址写
- 利用
_IO_str_jumps
或者_IO_wstr_jumps
这两个现成的vatble
任意地址读
要求是能够修改stdin
- 设置
_IO_read_end
等于_IO_read_ptr
。 - 设置
_flag &~ _IO_NO_READS
即_flag &~ 0x4
。 - 设置
_fileno
为0。 - 设置
_IO_buf_base
为write_start
,_IO_buf_end
为write_end
;且使得_IO_buf_end-_IO_buf_base
大于fread要读的数据。
2.27保护措施
在2.31环境下,通过篡改top chunk size从而将其放入UB的操作还是存在的,但是要注意会多出一个0x290大小的堆块,注意fake_size大小的计算。
但是2.27之后,malloc_printerr里刷新流的操作没了,此时的调用栈变成了这样。因此house of orange就失效了。