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")。

相关推荐
学逆向的7 小时前
C 多文件开发
c语言·开发语言·网络安全
学逆向的8 小时前
C 内存属性
c语言·开发语言·算法·网络安全
万联WANFLOW8 小时前
让分散的设备像在同一张局域网里:企业组网技术解析
运维·网络·网络安全·智能路由器
2501_9151063211 小时前
跨平台抓包工具怎么选?TraceEagle抓包鹰 的四种跨平台能力
网络协议·计算机网络·网络安全·ios·adb·https·udp
愤怒火龙果12 小时前
蓝队笔记整理04 安全设备、内网处置与云上防护
笔记·网络安全
持敬chijing16 小时前
系统入侵与攻击工具-namp扫描方式与内置脚本
网络·安全·网络安全·网络攻击模型·安全威胁分析
Bruce_Liuxiaowei16 小时前
2026年10月第4周网络安全形势周报
安全·web安全·网络安全
特别关注外国供应商17 小时前
Claroty Claire:OT 环境中的 AI 驱动型网络安全
人工智能·网络安全·ai安全·工业网络安全·claroty·ot网络安全·暴露面管理
青藤云安全18 小时前
AI攻防备战报告(运营商篇)|攻击面 “3+1” 解析:通信关基面临的新型 AI 安全风险
人工智能·网络安全·ai
zbtlink18 小时前
从 DDR4 颗粒到路由器价签:一轮存储涨价的三层传导与复算
网络·网络安全·智能路由器