House of Some 2是独立的一条IO_FILE利用链,主要关注的函数是_IO_wfile_jumps_maybe_mmap
中的_IO_wfile_underflow_maybe_mmap
利用条件为
- 已知libc地址
- 可控地址(可写入fake file)
- 可控stdout指针或者
_IO_2_1_stdout_
结构体 - 程序具有printf或者puts输出函数
优点如下
- 与House of Some一样可以绕过目前的vtable检查
- printf和puts比较普遍,适用性广
- 可以在栈上劫持控制流,衔接House of Some,完成最后攻击
原理描述
前置知识
首先我们先关注_IO_wfile_underflow_maybe_mmap
函数
1 | wint_t |
这个函数最后调用了_wide_data
内的虚表_IO_WUNDERFLOW
那么继续深入_IO_file_underflow_maybe_mmap
函数
1 | int |
这个函数最后调用了FILE的虚表_IO_UNDERFLOW
继续深入decide_maybe_mmap
函数
1 | static void |
这个函数有一个关键的_IO_SYSSTAT
调用,以及,在这个函数最后会恢复FILE和_wide_data
的虚表
整理一下可以知道,如果一个FILE进入了函数_IO_wfile_underflow_maybe_mmap
,那么他将会运行如下的流程
_IO_SYSSTAT(fp, &st)
调用虚表,传入栈指针- decide_maybe_mmap函数结束,恢复两个虚表
_IO_UNDERFLOW (fp)
调用虚表_IO_WUNDERFLOW (fp)
调用虚表
以及补充的条件
在_IO_file_jumps
虚表的_IO_UNDERFLOW
函数中
1 | count = _IO_SYSREAD (fp, fp->_IO_buf_base, |
这一步,三个参数都可控,也就是可以写入任意地址
最后我们需要补充一下IO_jump_t
结构体的全貌
1 | /* offset | size */ type = struct _IO_jump_t { |
第一次猜想
在printf和puts函数中,最后会调用stdout的__xsputn
虚表的入口
如果我们使得__xsputn
的偏移直接指向__underflow
呢?
那么就会得到如下的偏移
1 | __xsputn -> __underflow |
此时,修改stdout的虚表为_IO_wfile_jumps_maybe_mmap-0x18
在上述调用过程中_IO_SYSSTAT(fp, &st)
这个函数就会变成write(fp, &st, ??)
如果我们能够控制rdx就好了,这里就能做到栈数据泄露
rdx的控制
很遗憾,在上述函数过程中,并没有涉及rdx的操作(注: 以Ubuntu GLIBC 2.35-0ubuntu3.1为例,后文相同)
能够控制的也就只有后续调用的_IO_UNDERFLOW (fp)
中的_IO_SYSREAD (fp, fp->_IO_buf_base,fp->_IO_buf_end - fp->_IO_buf_base);
可以控制,由于decide_maybe_mmap
会强制恢复虚表,所以这里我们不用担心篡改虚表带来的影响
如果rdx不可控直接执行write(fp, &st, ??)
会怎么样,返回0或者非0
那么回到decide_maybe_mmap
1 | if (_IO_SYSSTAT (fp, &st) == 0 |
这里判断,如果_IO_SYSSTAT (fp, &st)
返回0,那么直接就不会进入if,如果返回不为0,我们看看S_ISREG
的定义
1 |
不必关注详细的值,这里可以看到最后判断采用的是==判断,由于栈上数据的限制,这里通过判断的概率不高
以及还有st.st_size != 0
判断,在没有正确执行stat逻辑,栈维持原貌的情况下,这个if通过概率不高
如果还高,可以控制fp->_offset == _IO_pos_BAD || fp->_offset <= st.st_size
为假即可
那么就能顺利的执行完decide_maybe_mmap
,并且保留伪造的fp内容没有任何变动
接下来就是调用_IO_file_jumps
虚表的_IO_UNDERFLOW
,操作执行read
这里,我们可以设置,注意fake_file_start就是我们当前控制的fp地址
1 | _IO_buf_base = fake_file_start |
那么,这里我们就能再次重新复写fake,并扩大可控长度,widedata都可控了
回到上面执行流程,接下来就会执行_IO_WUNDERFLOW (fp)
这个虚表函数了
然而,上述我们通过underflow重新控制了fp,也就是接下来的这个虚表函数,我们也是可控的
这里我们控制为
1 | _IO_WUNDERFLOW(fp) -> _IO_wfile_underflow_maybe_mmap |
回到起点
我们再次回到了起点,但是这次不一样了
在上一个小节,其实我们已经控制了rdx,因为_IO_SYSREAD (fp, fp->_IO_buf_base,fp->_IO_buf_end - fp->_IO_buf_base);
的第三个参数
1 | rdx = fp->_IO_buf_end - fp->_IO_buf_base |
此时,此时我们依然有这四个执行流程
_IO_SYSSTAT(fp, &st)
调用虚表,传入栈指针- decide_maybe_mmap函数结束,恢复两个虚表
_IO_UNDERFLOW (fp)
调用虚表_IO_WUNDERFLOW (fp)
调用虚表
不同的是,此时_IO_SYSSTAT(fp, &st)
可以被指向任意的虚表函数,因为在第二次控制fp的时候,我们又一次覆写了FILE的vtable
那么此时我们就可以控制
1 | _IO_SYSSTAT(fp, &st) -> _IO_new_file_read(fp, &st, rdx) |
我们已经成功完成了栈溢出
还有高手?Canary
很不幸,decide_maybe_mmap函数开启了canary,我们没办法在没有泄露栈的情况下,完成栈溢出
由于fileno的设置,无法完成write(1,stack,rdx)的操作,真的没有办法的了吗
那么接下来,有请_IO_default_xsputn
和_IO_default_xsgetn
我们阅读这两个函数源码
1 | size_t |
可以知道,这是对于fp内的缓冲区的操作,可以关注到的是这里函数内有两个关键的部分
1 | _IO_default_xsgetn (FILE *fp, void *data, size_t n) |
如果能够保证
1 | fp->_IO_read_end - fp->_IO_read_ptr == n |
就不会进入__underflow
和_IO_OVERFLOW
降低其他函数的干扰
这个时候就能衍生出一个大胆的想法,如果我们先将栈复制一份到可控的区域,再通过偏移写入,最后再拷贝回到栈内,那么我们就能完美的绕过canary并且,并不需要泄露canary
最后
又到了喜闻乐见的模板环节,Some1模板见https://github.com/CsomePro/Some-of-House
相关偏移为Ubuntu GLIBC 2.35-0ubuntu3.1版本glibc
1 | from pwn import * |
附录
demo程序
1 | // gcc demo.c -o demo |
作者: Csome