0%

IO_FILE源码分析:FSOP

IO_FILE源码分析:FSOP

vtable

IO_FILE结构体里面有个很重要的数据结构 —— vtable

vtable 采用虚表调用的形式,如果劫持了 vtable ,就可以调用我们需要的任意函数,甚至可以伪造 vtable ,使程序错误使用我们构造的 fake vtable

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
pwndbg> p/x*(struct _IO_FILE_plus*)_IO_list_all
$1 = {
file = {
_flags = 0xfbad2086,
_IO_read_ptr = 0x0,
_IO_read_end = 0x0,
_IO_read_base = 0x0,
_IO_write_base = 0x0,
_IO_write_ptr = 0x0,
_IO_write_end = 0x0,
_IO_buf_base = 0x0,
_IO_buf_end = 0x0,
_IO_save_base = 0x0,
_IO_backup_base = 0x0,
_IO_save_end = 0x0,
_markers = 0x0,
_chain = 0x7ffff7dd2620, // 指向下一个链表节点(stderr的下一个:stdout)
_fileno = 0x2, // fileno值为2(标准错误流的文件描述符就是'2')
_flags2 = 0x0,
_old_offset = 0xffffffffffffffff,
_cur_column = 0x0,
_vtable_offset = 0x0,
_shortbuf = {0x0},
_lock = 0x7ffff7dd3770,
_offset = 0xffffffffffffffff,
_codecvt = 0x0,
_wide_data = 0x7ffff7dd1660,
_freeres_list = 0x0,
_freeres_buf = 0x0,
__pad5 = 0x0,
_mode = 0x0,
_unused2 = {0x0 <repeats 20 times>}
},
vtable = 0x7ffff7dd06e0 // target
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
pwndbg> p*(struct _IO_jump_t*)_IO_list_all.vtable // vtable的所有字段都可以劫持
$6 = {
__dummy = 0,
__dummy2 = 0,
__finish = 0x7ffff7a869d0 <_IO_new_file_finish>,
__overflow = 0x7ffff7a87740 <_IO_new_file_overflow>,
__underflow = 0x7ffff7a874b0 <_IO_new_file_underflow>,
__uflow = 0x7ffff7a88610 <__GI__IO_default_uflow>,
__pbackfail = 0x7ffff7a89990 <__GI__IO_default_pbackfail>,
__xsputn = 0x7ffff7a861f0 <_IO_new_file_xsputn>,
__xsgetn = 0x7ffff7a85ed0 <__GI__IO_file_xsgetn>,
__seekoff = 0x7ffff7a854d0 <_IO_new_file_seekoff>,
__seekpos = 0x7ffff7a88a10 <_IO_default_seekpos>,
__setbuf = 0x7ffff7a85440 <_IO_new_file_setbuf>,
__sync = 0x7ffff7a85380 <_IO_new_file_sync>,
__doallocate = 0x7ffff7a7a190 <__GI__IO_file_doallocate>,
__read = 0x7ffff7a861b0 <__GI__IO_file_read>,
__write = 0x7ffff7a85b80 <_IO_new_file_write>,
__seek = 0x7ffff7a85980 <__GI__IO_file_seek>,
__close = 0x7ffff7a85350 <__GI__IO_file_close>,
__stat = 0x7ffff7a85b70 <__GI__IO_file_stat>,
__showmanyc = 0x7ffff7a89b00 <_IO_default_showmanyc>,
__imbue = 0x7ffff7a89b10 <_IO_default_imbue>
}

下面是 raycp 大佬对 vtable 调用的总结

fread 函数中调用的 vtable 函数有:

  • _IO_sgetn函数调用了vtable的_IO_file_xsget
  • _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函数

FSOP

终于到今天的主角了,FSOP全称是 File Stream Oriented Programming ,它的核心就在于伪造 _IO_list_all 指针( _IO_list_all 永远指向当前FILE结构体链的第一个元素)

  • 这个技术的核心就是劫持 _IO_list_all 的值来伪造链表和其中的 _IO_FILE 项,但是单纯的伪造只是构造了数据还需要某种方法进行触发
  • FSOP 选择的触发方法是调用 _IO_flush_all_lockp,这个函数会刷新 _IO_list_all 链表中所有项的文件流(相当于对每个FILE调用 fflush),也对应着会调用 _IO_FILE_plus.vtable 中的 IO_overflow 函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
_IO_flush_all_lockp (int do_lock)
{
int result = 0;
FILE *fp;
#ifdef _IO_MTSAFE_IO
_IO_cleanup_region_start_noarg (flush_cleanup);
_IO_lock_lock (list_all_lock);
#endif
for (fp = (FILE *) _IO_list_all; fp != NULL; fp = fp->_chain)
{
run_fp = fp;
if (do_lock)
_IO_flockfile (fp);
if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)/*一些检查,需要绕过*/
|| (_IO_vtable_offset (fp) == 0
&& fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
> fp->_wide_data->_IO_write_base))/*也可以绕过这个*/
)
&& _IO_OVERFLOW (fp, EOF) == EOF)/*遍历_IO_list_all ,选出_IO_FILE作为_IO_OVERFLOW的参数,执行函数*/
result = EOF;
if (do_lock)
_IO_funlockfile (fp);
run_fp = NULL;
}
#ifdef _IO_MTSAFE_IO
_IO_lock_unlock (list_all_lock);
_IO_cleanup_region_end (0);
#endif
return result;
}

IO_flush_all_lockp 函数触发条件:

  • 当 libc 执行 abort 流程时,abort 可以通过触发 malloc_printerr 来触发
  • 当执行 exit 函数时
  • 当执行流从 main 函数返回时

FSOP 攻击的前提条件:

  • 泄露出 libc 地址,知道 _IO_lsit_all 的地址
  • 任意地址写的能力,修改 _IO_list_all 为可控的地址
  • 可以在可控内存中伪造 _IO_FILE_plus 结构

伪造的 _IO_FILE_plus 结构体要绕过的 check:

1
2
3
1.((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)
或者是
2._IO_vtable_offset (fp) == 0 && fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base)

FSOP 利用姿势

在考虑进行 FSOP 攻击时,常常不会有可控的 WAA,这时我们利用 unsortedbin attack 在 _IO_list_all 中写入 main_arena + 88 的地址,那么程序就会把 main_arena + 88 当做FILE结构体

然后 main_arena + 0xc0(88+0x68) 的位置就是FILE结构体的 _chain 条目,同时,main_arena + 0xc0 也会指向对应大小为“0x60”的 smallbin,那么大小为“0x60”的smallbin就会被程序当成下一个FILE结构体

到了这里通常有两种应对方式,一是直接申请大小为“0x60”的chunk,把它放入对应 smallbin 的头部,然后在其内部进行伪造,二是通过堆溢出等方式,修改 unsortedbin 的size为“0x61”(注意规避合并机制),再次申请 chunk,该 unsortedbin 就会被放入 smallbin

接下来就是对 FILE 结构体的伪造,只需要伪造 vtable 的指向地址,然后再该地址处写入 fake vtable 就可以了

而在对 vtable 的伪造中,主要是伪造 _IO_OVERFLOW 条目(第4个),因为程序对FILE结构的破坏,导致程序会触发报错提示,根据程序调用链发现中途会调用 _IO_OVERFLOW ,正好可以被控制

这就是 FSOP 的全过程了,可以去学习 house of orange 进行巩固


文字描述有点多,需要根据题目过一遍