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 | context.clear(arch='amd64') |
例题
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就失效了。
