House of Orange

"House of Orange" 是在 CTF(网络安全夺旗赛)和真实漏洞利用中非常经典且高级的一种堆溢出利用技术。它的名字来源于 2016 年 HITCON CTF 比赛中的一道同名题目。

为什么会有 House of Orange?(它的核心亮点)

在常规的黑客攻击中,如果你想利用内存里的"堆(Heap)"干坏事,通常需要程序里有一个 free() 函数(把内存还给系统的函数)。但有些程序写得很"死",它只允许你申请内存(malloc),根本没有提供释放内存(free)的功能。

House of Orange 的出现就是为了打破这个限制:它能在没有 free函数的情况下,强行制造出一个被释放的内存块,从而完成后续的攻击。


基础知识铺垫(你必须知道的 3 个概念)

为了看懂这个魔法,你需要知道 Linux 系统分配内存时的三个设定:

  1. Top Chunk(顶层块):
    想象系统最初给了你一块巨大无比的蛋糕(堆内存)。每次程序调用 malloc 申请内存,系统就从这块大蛋糕上切一块给你。切剩下来的那一块巨大的、还没有被分配出去的内存,就叫 Top Chunk。
  2. sysmalloc(系统扩容):
    如果你某次狮子大开口,要申请一块巨大的内存,但当前的 Top Chunk 已经不够切了怎么办?系统会触发一个叫 sysmalloc 的机制,重新向操作系统申请一块新的大蛋糕。
  3. Unsorted Bin(垃圾桶):
    在触发 sysmalloc 申请新蛋糕时,系统是一个很节约的人,它不会把原来那块没用完的旧 Top Chunk 直接扔掉,而是把它放进一个叫 Unsorted Bin 的垃圾桶(回收站)里,留着以后给那些申请小块内存的需求用。

重点来了: 只要一块内存进入了 Unsorted Bin,它就等同于被 free(释放)了!这就是 House of Orange 的突破口。


House of Orange 的攻击四部曲

现在我们来看看,黑客是如何一步步无中生有,拿下服务器权限的。

第一步:篡改 Top Chunk 的大小(骗过系统)

程序里存在一个堆溢出 漏洞。黑客利用这个漏洞,在往某块已经申请的内存里写数据时,故意写得太长,越界覆盖到了紧挨着它的 Top Chunk 的头部数据。

Top Chunk 的头部记录着它自己的大小(Size)。黑客通过溢出,把原本比如有 131072 字节(0x20000)大小的 Top Chunk,强行修改成了一个很小的值,比如 4096 字节(0x1000)。

新手注意: 这个假大小不能随便瞎写,必须满足系统的检查条件,主要是:

  • 必须大于最小限制(通常是 0x20)。
  • 必须和系统的内存页(Page)对齐,通常大小要是 4096 的倍数(末尾是 000)。
  • 它的"前一个块正在使用"标志位(PREV_INUSE)必须是 1。

第二步:触发扩容,获得"释放"的内存

现在系统以为当前的 Top Chunk 只剩 4096 字节了。这时,黑客操控程序,强行申请一块大于 4096 字节的内存(比如申请 4097 字节)。

系统一看:哎呀,Top Chunk 只有 4096 了,不够切啊!于是系统乖乖地触发了 sysmalloc 去要新蛋糕,顺手就把我们那个被篡改过大小的旧 Top Chunk 扔进了 Unsorted Bin 里 。

至此,黑客在没有调用任何 free函数的情况下,成功获得了一块被释放的内存!

第三步:Unsorted Bin Attack(改写关键指针)

旧 Top Chunk 进垃圾桶后,黑客通过漏洞再次修改这块垃圾桶里的内存,把它内部的指针(bk 指针)指向系统内核中一个极其敏感的地方------_IO_list_all。

(_IO_list_all 是 Linux 系统用来记录所有输入输出流,比如标准输入、标准错误输出的链表头。)

当系统下次再去整理垃圾桶(Unsorted Bin)时,会触发一个漏洞机制,不小心把垃圾桶在系统里的地址,强行写到了 _IO_list_all里面。这就好比黑客把系统内部的高铁调度中心,强行指向了黑客自己搭建的一条野鸡铁轨上。

第四步:FSOP 劫持程序流(最终绝杀)

这是最复杂但也是最致命的一步,被称为 FSOP (File Stream Orientation Programming)。

黑客在堆内存里提前伪造好了一个假的"文件流结构体(_IO_FILE)"和一套"假的执行函数表(vtable)"。因为第三步已经把 _IO_list_all 指向了堆里,系统现在会把黑客伪造的数据当成合法的文件操作流。

接着,黑客故意在系统中制造一个内存崩溃错误(比如再次破坏堆的结构)。当系统发现堆坏了,想要通过标准错误流(stderr)打印一句"Malloc: memory corrupted!"这样的报错信息时,它会去读取 _IO_list_all。

这一读就中计了!系统顺着黑客伪造的结构体,找到了黑客伪造的函数表,本来是想执行"打印报错"的函数,结果却执行了黑客提前写好的 system("/bin/sh")。

瞬间,黑客拿到了服务器的 Shell 控制台,游戏结束。


总结一下流程

对于 0 基础的你,只需要在脑海里建立这样一条逻辑链:

  1. 没有 free? -> 用堆溢出修改 Top Chunk 大小,骗它说蛋糕不够了。
  2. 触发 sysmalloc! -> 申请大内存,强迫系统把旧 Top Chunk 扔进回收站(等同于 free)。
  3. 改写 _IO_list_all! -> 利用回收站的机制,把系统的关键 IO 链表指向黑客控制的区域。
  4. 报错触发后门! -> 故意制造错误让系统打印信息,系统在打印时使用被篡改的 IO 链表,最终执行了黑客的恶意命令。

补充一点历史背景: 这种技术在 Glibc 2.23 和 2.24 版本中非常流行且无解。但在后来的 Glibc 2.26+ 甚至更高的版本中,系统增加了各种安全检查(比如引入了 tcache 以及对 vtable 进行了严格校验),原汁原味的 House of Orange 已经很难直接打通了,但这依然是学习高级堆漏洞利用必不可少的里程碑式技术。

例子

为了让你直观地看到这个"魔法"是如何在代码层面发生的,我们通过一个极简的 C 语言示例,带你走完 House of Orange 最核心的前两步:如何利用堆溢出,无中生有地制造出被释放的内存(Unsorted Bin)。

在真实的 Linux ptmalloc 内存管理器中,要骗过系统并不容易。我们直接来看代码和内存布局的真实变化。

1. 存在漏洞的 C 代码(靶场环境)

假设我们有下面这段存在堆溢出漏洞的 C 代码:

复制代码
#include <stdio.h>
#include <stdlib.h>

int main() {
    // 1. 程序分配了一小块内存
    // malloc(0x10) 实际在内存中会分配 0x20 字节(包含 0x10 的头部控制信息)
    char *p1 = malloc(0x10);
    
    // ==========================================
    // 此时的内存布局:
    // [ p1 chunk 头部 (0x10 字节) ]
    // [ p1 数据区 (0x10 字节)      ] <--- p1 指针指向这里
    // [ Top Chunk 头部 (Size: 0x20fe1) ] <--- 紧挨着 p1
    // [ Top Chunk 巨大的剩余空间... ]
    // ==========================================

    // 2. 黑客利用堆溢出漏洞,越界修改紧挨着的 Top Chunk 大小
    // 注意:这里的 0x0fe1 不是随便写的,必须满足系统的严格检查
    unsigned long *top_chunk_header = (unsigned long *)(p1 + 0x10);
    *top_chunk_header = 0x0fe1; 
    
    // 3. 强行申请一块大内存,触发系统扩容 (sysmalloc)
    char *p2 = malloc(0x1000);

    return 0;
}

2. 黑客视角的详细拆解

动作一:精心计算并修改 Top Chunk 大小

在代码的第 2 步,我们把 Top Chunk 的 Size 从默认的 0x20fe1(大约 135 KB)改成了 0x0fe1(4065 字节)。

在 ptmalloc 的底层机制中,这个伪造的大小必须满足三个严苛条件,否则程序会直接崩溃(Segmentation Fault):

  1. 必须大于最小限制: 大于 MINSIZE(通常是 0x20)。0x0fe1 满足。
  2. 前一个块必须是使用中: 最低位(PREV_INUSE)必须是 1。0x0fe1 的二进制最低位是 1,满足。
  3. 必须与内存页(Page)对齐: 这是最难的一点。公式是 (Top_Chunk_首地址 + Top_Chunk_Size) & 0xfff == 0。
  • 假设堆的起始地址是 0x555555602000。
  • p1 占用了 0x20 字节,所以 Top Chunk 的首地址是 0x555555602020。
  • 0x555555602020 + 0x0fe1 = 0x555555603001。(注意:实际计算对齐时会忽略标志位,即加 0x0fe0,结果是 0x555555603000)。
  • 0x555555603000 的末尾是三个 0(即 4096 的倍数),完美对齐内存页!

动作二:触发 sysmalloc 榨干 Top Chunk

在代码的第 3 步,程序调用了 malloc(0x1000)。

系统内核接到请求后开始思考:

  1. 客户要 0x1000 字节。
  2. 我去看看 Top Chunk 还有多少... 咦?只有 0x0fe0 字节了(扣除标志位)。
  3. 0x0fe0 < 0x1000,蛋糕不够切了!
  4. 系统被迫调用 sysmalloc 向操作系统申请新的一大块内存。
  5. 关键漏洞爆发: 秉承着不浪费的原则,系统把旧的这个 0x0fe1 大小的 Top Chunk,直接丢进了 Unsorted Bin(回收站)里。

至此,虽然整个 C 程序里连一个 free() 函数都没有写,但黑客已经在内存里拿到了一块合法的、被释放的 Unsorted Bin Chunk。


3. 后续的绝杀(Python Exploit 伪代码)

拿到这块进入垃圾桶的内存后,黑客会再次利用漏洞向里面写数据。在真实的 CTF 比赛(如使用 Pwntools)中,构造出来的攻击载荷(Payload)大致长这样:

复制代码
# 假设我们可以再次向旧的 Top Chunk(现在的 Unsorted Bin)写入数据

# 1. 伪造 Chunk 头部信息
payload = p64(0) + p64(0x61)  

# 2. Unsorted Bin Attack 核心:改写 bk 指针
# 把垃圾桶的后向指针指向系统 IO 链表头 _IO_list_all 的地址减去 0x10
payload += p64(0) + p64(_IO_list_all - 0x10)

# 3. 伪造 _IO_FILE 结构体 (FSOP)
payload += p64(0) + p64(1)    # 绕过底层的 assert 检查
payload += p64(0) * 7         # 填充无关数据
payload += p64(system_addr)   # 偷偷塞入 system 函数的地址

# 4. 伪造 vtable (虚函数表)
# 把系统本该调用的 _IO_OVERFLOW (比如打印报错信息) 强行指向我们的 system 函数
payload += p64(fake_vtable_addr)

# 发送攻击载荷覆盖内存
send(payload)

# 再次发起一个会导致堆崩溃的 malloc,触发系统向终端打印报错,瞬间拿到 shell
malloc(0x10) 

一旦系统因为堆结构被破坏而尝试调用 _IO_OVERFLOW 打印错误信息,就会顺着被我们污染的 _IO_list_all,读到伪造的虚函数表,最终将原本用于报错的字符串 "/bin/sh" 作为参数,执行了 system("/bin/sh")。

相关推荐
Seraphina364 小时前
DVWA(SQL注入-low,medium,XSS反射-low)
前端·数据库·笔记·sql·网络安全·web·xss
菩提小狗9 小时前
每日安全情报报告 · 2026-10-03
网络安全·漏洞·cve·安全情报·每日安全
愤怒火龙果10 小时前
AI攻防 外部资源加载利用
人工智能·网络安全
Eason_LYC12 小时前
一个公开密钥放倒一片:Apache Shiro RememberMe 反序列化漏洞
java·网络安全·渗透测试·shiro·漏洞复现·反序列化·ai安全
剑胆凌锋21 小时前
等级保护测评工程师的困境
网络·安全·web安全·网络安全·职场和发展
-dzk-1 天前
【堆】LC 347.前 K 个高频元素
算法·堆
小小小小钰儿1 天前
2.3-云端API集成
人工智能·计算机·网络安全·操作系统·编程
白帽攻防录1 天前
SRC 挖洞:Dgraph GraphQL 数据库注入深度复盘,CVE-2026-41328 两个请求拖走全库数据
网络安全·graphql
小小小小钰儿1 天前
2.2-模型微调
人工智能·深度学习·机器学习·计算机·网络安全·操作系统·编程