House Of Orange + Unsorted Bin Attack

什么是 House Of Orange?

Make something out of nothing

首先 假设我们的堆布局如下:

1
2
graph TB
A(Chunk)-.-B(Top Chunk)

而且 我们知道 如果此时的Chunk存在堆溢出状况 我们就可以覆盖Top chunk 的 Prev-Size 和 Size 位

知道了这些,我们就可以堆Top-chunk的源码进行分析了

Sources

有关Top-chunk的分割 在glibc源码中有说明(glibc 2.29) 使用Top-chunk的前提是bin中没用合适使用的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
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
    .......
use_top:
/*
If large enough, split off the chunk bordering the end of memory
(held in av->top). Note that this is in accord with the best-fit
search rule. In effect, av->top is treated as larger (and thus
less well fitting) than any other available chunk since it can
be extended to be as large as necessary (up to system
limitations).

We require that av->top always exists (i.e., has size >=
MINSIZE) after initialization, so if it would otherwise be
exhausted by current request, it is replenished. (The main
reason for ensuring it exists is that we may need MINSIZE space
to put in fenceposts in sysmalloc.)
*/

victim = av->top;
si·ze = chunksize (victim);

if (__glibc_unlikely (size > av->system_mem))
malloc_printerr ("malloc(): corrupted top size");

if ((unsigned long) (size) >= (unsigned long) (nb + MINSIZE))
{
remainder_size = size - nb;
remainder = chunk_at_offset (victim, nb);
av->top = remainder;
set_head (victim, nb | PREV_INUSE |
(av != &main_arena ? NON_MAIN_ARENA : 0));
set_head (remainder, remainder_size | PREV_INUSE);

check_malloced_chunk (av, victim, nb);
void *p = chunk2mem (victim);
alloc_perturb (p, bytes);
return p;
}

/* When we are using atomic ops to free fast chunks we can get
here for all block sizes. */
else if (atomic_load_relaxed (&av->have_fastchunks))
{
malloc_consolidate (av);
/* restore original bin index */
if (in_smallbin_range (nb))
idx = smallbin_index (nb);
else
idx = largebin_index (nb);
}

/*
Otherwise, relay to handle system-dependent cases
*/
else
{
void *p = sysmalloc (nb, av);
if (p != NULL)
alloc_perturb (p, bytes);
return p;
}
}
}

需要注意的是,在glibc2.29或更早的use_top中新添加了对于另一个size的判断,我们暂时忽略

可以看到在分割 Top_chunk 时,会首先对于Top-chunk的大小,也就是size和用户需要的的大小也就是nb进行判断,如果用户申请的空间小于等于Top-chunk size 会进行接下来的操作:

  • 重新计算Top-chunk的大小 也就是 remainder_size = size - nb
  • 获取一个Topchunk + nb的的chunk 其中nb为的用户申请的大小, 赋值为Remainder
  • 把Top-chunk赋值为Remainder
  • 申请用户内存

但是 如果 nb (用户申请大小) 不满足要求 那么会使用 sysmalloc申请内存

Sysmalloc()

在这里 我们遇到第一个条件判断

1
2
3
4
5
if (av == NULL
|| ((unsigned long) (nb) >= (unsigned long) (mp_.mmap_threshold)
&& (mp_.n_mmaps < mp_.n_mmaps_max)))
{
char *mm;
  • av *(arena)* == NULL,arena为空,或者
  • 申请内存大小大于mmap_threshold

在这两种情况下,会调用mmap申请内存 如果 不满足条件 则会转到else分支

else 分支首先会对 Top-chunk进行检查:

1
2
3
4
5
6
7
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));

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

在这里 Top-chunk必须满足以下的条件:

  • Topchunk 的 prev-inuse 必须为 1
  • Topchunk size >= MINSIZE
  • old_end 为页对齐 old_end = Topchunk address + Top-chunk-size

如果这些条件都满足,那么会尝试扩展Topchunk (不放源码了,无聊) 一般来说分两种情况:arena 不是 主线程 和 arena 是 主线程

其中的具体操作不说明了 反正两种分支都会free掉源Topchunk

那么好玩的就来了:如果我们可以通过修改Topchunk,绕过前面的检查,我们就可以修改size,导致最后被free的是一个unsorted bin

1
2
3
4
5
	/* If possible, release the rest. */
if (old_size >= MINSIZE)
{
_int_free (av, old_top, 1);
}

how to exploit?

需要绕过检查的条件,稍微总结一下:

  • nb > Topchunk->size
  • Topchunk -> prev-inuse 为 1
  • Topchunk size >= MINSIZE
  • old_end 为页对齐

也就是说我们只要在溢出时满足了这些条件 那么我们申请出来的必定是Unsorted Bin 至此,我们已经成功Make something out of nothing了 (在CTF-WIKI中,对于H-O-O的介绍只有这些。以下的内容是比较偏IO_FILE 攻击的,可以选看)

Make Something into Everything

我们先留个悬念,不讲什么,先从一个叫做malloc_printerr函数说起

1
2
3
4
5
6
7

static void malloc_printerr (const char *str)
{
__libc_message (do_abort, "%s\n", str);
__builtin_unreachable ();
}

malloc_printerr 是一个错误提示函数,主要会在malloc发生错误时被调用

可以看到,在我们熟悉的unlink中也有他的身影

1
2
3
4
5
static void
unlink_chunk (mstate av, mchunkptr p)
{
if (chunksize (p) != prev_size (next_chunk (p)))
malloc_printerr ("corrupted size vs. prev_size");

其中 它调用了一个__libc_message函数 其中在这个函数的80-90行,调用了abort函数

1
2
3
4
5
6
7
8
 if ((action & do_abort))
{
if ((action & do_backtrace))
BEFORE_ABORT (do_abort, written, fd);

/* Kill the application. */
abort ();
}

其中,它还调用了fflush()函数 而这个函数,就很有趣了。

首先,我们如果阅读源码,是可以发现fflush() 本质上就是一个调用链

1
2
graph LR
a[fflush]-->b[_IO_fflush]-->c[_IO_flush_all]-->d[_IO_flush_all_lockp]

这个时候,我们去最后调用的_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

int
_IO_flush_all_lockp (int do_lock)
{
int result = 0;
FILE *fp;

#ifdef _IO_MTSAFE_IO
_IO_cleanup_region_start_noarg (flush_cleanup);
_IO_lock_lock (list_all_lock);
#endif

for (fp = (FILE *) _IO_list_all; fp != NULL; fp = fp->_chain) /*Notice the _IO_list_all*/
{
run_fp = fp;
if (do_lock)
_IO_flockfile (fp);

if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base)
|| (_IO_vtable_offset (fp) == 0
&& fp->_mode > 0 && (fp->_wide_data->_IO_write_ptr
> fp->_wide_data->_IO_write_base))
)
&& _IO_OVERFLOW (fp, EOF) == EOF) /*#define _IO_OVERFLOW(FP, CH) JUMP1 (__overflow, FP, CH)*/
result = EOF;

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

这个函数,简单来说就是以list_all_lock为链表头进行遍历,而且对一些条件进行判断。最后会调用一个_IO_OVERFLOW函数,而这个函数其实就是jump到了__overflow函数 其中 这个函数调用一些和IO_FILE有关的函数 我们看看IO_file链式结构

IO_FILE Structure

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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;

# codes......

可以看到 在这个结构中,出现了一个*_chain 成员, 这个成员的作用是连接下一个IO_FILE

同时,我们在_IO_flush_all_lockp函数中,可以看到我们作为遍历头指针的_IO_list_all,他的作用不想而之,就像如下

1
2
3
4
5
6
7
8
9
10
11
12
struct _IO_FILE_plus *_IO_list_all = &_IO_2_1_stderr_;
libc_hidden_data_def (_IO_list_all)

/*
The define of _IO_FILE_plus
*/

struct _IO_FILE_plus
{
_IO_FILE file;
const struct _IO_jump_t *vtable;
};

会形成一个这样的链表:

1
2
graph LR
a[ptr *_IO_List_all]-->top[IO_FILE_stderr]==>IO_FILE_stdin==>IO_FILE_stdout

现在,我知道我们可以通过Make Something out of nothing部分的方法,通过Top chunk 的方法“分割“出来一个Unsorted Bin 那么,也就说明我们可以通过尝试Unsorted Bin attack 的方法, 去修改_IO_list_all指针。Unsorted Bin attack的结果也不用多说,可以修改任意地址变成一个大数字,也就是unsorted bin的地址。

算了。想一想还是讲一下Unsorted Bin Attack

Unsorted Bin Attack

这一切都要从一段源码说起;

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

_int_malloc 有这么一段代码,当将一个 unsorted bin 取出的时候,会将 bck->fd 的位置写入本 Unsorted Bin 的位置。

那么也就是说,如果我们可以控制到当前堆块的bk,那么我们就可以把地址为bk(不准确)的地址修改成unsorted_chunks (av);

这里,我希望大家可以在How2Heap看一看Unsorted Bin attack的例子,也推荐大家动态调试一下。


接下来,我们回到我们之前的思路,可以知道,我们可以通过Unsorted Bin Attack_IO_list_all指针修改成Main Arena + 88 (unsorted_chunks (av);)的地址,那么,如果我们把Main Arena + 88当作一个IO_FILE结构体,那么根据星梦大佬的说法来说,IO_File结构体中的*_chain指针就等于Main Arena + 88 + 0x68 。也就等于Main Arena + 0xc0,而这个地址就正好就是0x60大小的Small chunk的地址。