前言

本文学习自CTF-Wiki的House of Orange, 根据学习情况略有补充和修改

介绍

House of Orange 与其他的 House of XX 利用方法不同,这种利用方法来自于 Hitcon CTF 2016 中的一道同名题目。由于这种利用方法在此前的 CTF 题目中没有出现过,因此之后出现的一系列衍生题目的利用方法我们称之为 House of Orange

概述

House of Orange 的利用比较特殊,首先需要目标漏洞是堆上的漏洞但是题目中不存在 free 函数或其他释放堆块的函数。我们知道一般想要利用堆漏洞,需要对堆块进行 malloc 和 free 操作,但是在 House of Orange 利用中无法使用 free 函数,因此 House of Orange 核心就是通过漏洞利用获得 free 的效果

原理

如我们前面所述,House of Orange 的核心在于在没有 free 函数的情况下得到一个释放的堆块 (unsorted bin)。 这种操作的原理简单来说是当前堆的 top chunk 尺寸不足以满足申请分配的大小的时候,原来的 top chunk 会被释放并被置入 unsorted bin 中,通过这一点可以在没有 free 函数情况下获取到 unsorted bins。

我们来看一下这个过程的详细情况,我们假设目前的 top chunk 已经不满足 malloc 的分配需求。 首先我们在程序中的malloc调用会执行到 libc.so 的 _int_malloc 函数中,在_int_malloc 函数中,会依次检验 fastbinsmall binsunsorted binlarge bins 是否可以满足分配要求,因为尺寸问题这些都不符合。接下来 _int_malloc 函数会试图使用 top chunk,在这里 top chunk 也不能满足分配的要求,因此会执行如下分支。

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

此时 ptmalloc 已经不能满足用户申请堆内存的操作,需要执行 sysmalloc 来向系统申请更多的空间。 但是对于堆来说有 mmap 和 brk 两种分配方式,我们需要让堆以 brk 的形式拓展,之后原有的 top chunk 会被置于 unsorted bin 中。

综上,我们要实现 brk 拓展 top chunk,但是要实现这个目的需要绕过一些 libc 中的 check。 首先,malloc 的尺寸不能大于 mmp_.mmap_threshold

if ((unsigned long)(nb) >= (unsigned long)(mp_.mmap_threshold) && (mp_.n_mmaps < mp_.n_mmaps_max))

如果所需分配的 chunk 大小大于 mmap 分配阈值,默认为 128K,并且当前进程使用 mmap() 分配的内存块小于设定的最大值,将使用 mmap() 系统调用直接向操作系统申请内存。

在 sysmalloc 函数中存在对 top chunk size 的 check,如下

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 的合法性,如果第一次调用本函数,top chunk 可能没有初始化,所以可能 old_size 为 0。 如果 top chunk 已经初始化了,那么 top chunk 的大小必须大于等于 MINSIZE,因为 top chunk 中包含了 fencepost,所以 top chunk 的大小必须要大于 MINSIZE。其次 top chunk 必须标识前一个 chunk 处于 inuse 状态,并且 top chunk 的结束地址必定是页对齐的。此外 top chunk 除去 fencepost 的大小必定要小于所需 chunk 的大小,否则在 _int_malloc() 函数中会使用 top chunk 分割出 chunk。

我们总结一下伪造的 top chunk size 的要求

  1. 伪造的 size 必须要对齐到内存页
  2. size 要大于 MINSIZE(0x10)
  3. size 要小于之后申请的 chunk size +
    MINSIZE(0x10)
  4. size 的 prev inuse 位必须为 1

之后原有的 top chunk 就会执行_int_free从而顺利进入 unsorted bin 中。

示例

这里给出了一个示例程序,程序模拟了一个溢出覆盖到 top chunk 的 size 域。我们试图把 size 改小从而实现 brk 扩展,并把原有的 top chunk 放入 unsorted bin 中。

#define fake_size 0x41

int main(void)
{
void *ptr;

ptr=malloc(0x10);
ptr=(void *)((int)ptr+24);

*((long long*)ptr)=fake_size; // overwrite top chunk size

malloc(0x60);

malloc(0x60);
}

这里我们把 top chunk 的 size 覆盖为 0x41。之后申请大于这个尺寸的堆块,即 0x60。 但是当我们执行这个示例时会发现,这个程序并不能利用成功,原因在于 assert 并没有被满足从而抛出了异常

[#0] 0x7ffff7a42428 → Name: __GI_raise(sig=0x6)
[#1] 0x7ffff7a4402a → Name: __GI_abort()
[#2] 0x7ffff7a8a2e8 → Name: __malloc_assert(assertion=0x7ffff7b9e150 "(old_top == initial_top (av) && old_size == 0) || ((unsigned long) (old_size) >= MINSIZE && prev_inuse (old_top) && ((unsigned long) old_end & (pagesize - 1)) == 0)", file=0x7ffff7b9ab85 "malloc.c", line=0x95a, function=0x7ffff7b9e998 <__func__.11509> "sysmalloc")
[#3] 0x7ffff7a8e426 → Name: sysmalloc(nb=0x70, av=0x7ffff7dd1b20 <main_arena>)

正确示例

我们回头来看一下 assert 的条件,可以发现之前列出的条目都被满足了除了第一条。

1.伪造的size必须要对齐到内存页

什么是对齐到内存页呢?我们知道现代操作系统都是以内存页为单位进行内存管理的,一般内存页的大小是 4kb。那么我们伪造的 size 就必须要对齐到这个尺寸。在覆盖之前 top chunk 的 size 大小是 20fe1,通过计算得知 0x602020+0x20fe0=0x623000 是对于 0x1000(4kb)对齐的。

0x602000:   0x0000000000000000  0x0000000000000021
0x602010: 0x0000000000000000 0x0000000000000000
0x602020: 0x0000000000000000 0x0000000000020fe1 <== top chunk
0x602030: 0x0000000000000000 0x0000000000000000

因此我们伪造的 fake_size 可以是 0x0fe1、0x1fe1、0x2fe1、0x3fe1 等对 4kb 对齐的 size。而 0x40 不满足对齐,因此不能实现利用。

#define fake_size 0x1fe1

int main(void)
{
void *ptr;

ptr=malloc(0x10);
ptr=(void *)((int)ptr+24);

*((long long*)ptr)=fake_size;

malloc(0x2000);

malloc(0x60);
}

进行分配之后我们可以观察到原来的堆经过了 brk 扩展

//原有的堆
0x0000000000602000 0x0000000000623000 0x0000000000000000 rw- [heap]

//经过扩展的堆
0x0000000000602000 0x0000000000646000 0x0000000000000000 rw- [heap]

我们的申请被分配到 0x623010 的位置,同时原有的堆被置入 unsorted bin

[+] unsorted_bins[0]: fw=0x602020, bk=0x602020
→ Chunk(addr=0x602030, size=0x1fc0, flags=PREV_INUSE)

因为 unsorted bin 中存在块,所以我们下次的分配会切割这个块

 malloc(0x60);
0x602030

[+] unsorted_bins[0]: fw=0x602090, bk=0x602090
→ Chunk(addr=0x6020a0, size=0x1f50, flags=PREV_INUSE)

可以看到分配的内存是从 unsorted bin 中切割的,内存布局如下

0x602030:   0x00007ffff7dd2208  0x00007ffff7dd2208 <== 未被清零的unsorted bin链表
0x602040: 0x0000000000602020 0x0000000000602020
0x602050: 0x0000000000000000 0x0000000000000000
0x602060: 0x0000000000000000 0x0000000000000000
0x602070: 0x0000000000000000 0x0000000000000000
0x602080: 0x0000000000000000 0x0000000000000000
0x602090: 0x0000000000000000 0x0000000000001f51 <== 切割剩下的新unsorted bin
0x6020a0: 0x00007ffff7dd1b78 0x00007ffff7dd1b78
0x6020b0: 0x0000000000000000 0x0000000000000000

例子

CISCN 2020 nofree