第94章 漏洞利用开发(四):堆喷射、Heap Feng Shui
堆喷射(Heap Spray)和堆风水(Heap Feng Shui),这两个名字听起来挺玄乎。说白了,它们都是堆漏洞利用中的核心技术。我最早接触堆喷射是在IE浏览器漏洞分析那会儿,当时觉得这玩意儿简直像魔法——往堆里撒一堆数据,就能控制程序执行流。后来自己动手写过几次,才明白背后的门道。
这一章,咱们就聊聊这两种技术。我会结合我踩过的坑,把核心思路讲清楚。
堆喷射:用数据淹没堆
堆喷射的核心思想很简单:在堆上大量分配特定模式的对象,让堆内存被可控数据填满。这样,当漏洞触发后,程序读取或执行了堆上的数据,我们就有了控制权。
为什么会需要堆喷射?你想想看,很多堆漏洞(比如UAF、堆溢出)的问题在于:我们不知道目标对象在堆上的精确位置。堆分配器有自己的算法,不同版本、不同运行状态下,分配结果都不一样。那怎么办?干脆把整个堆区域都喷上我们的数据,不管漏洞跳到哪里,都能命中。
堆喷射的典型场景:
- 浏览器漏洞利用(IE、Chrome、Firefox)
- ActiveX控件漏洞
- PDF阅读器漏洞
- Flash插件漏洞
我记得有一次分析一个IE的UAF漏洞,漏洞触发后程序会从一个已经释放的对象上读取虚函数表指针。当时我试了各种方法去占位,都不稳定。后来改用堆喷射,在堆上铺满伪造的虚表对象,成功率直接拉到90%以上。
堆喷射的经典实现
在JavaScript环境下,堆喷射最常用的手法就是分配大量字符串。每个字符串内部包含NOP sled + shellcode + 地址填充。来看一个简化版示例:
// 堆喷射 - JavaScript 示例
var spray = function() {
var shellcode = unescape(
"%u9090%u9090" + // NOP sled
"%u1234%u5678" + // shellcode 占位
// ... 实际shellcode
);
// 构造喷射块
var block = "";
while (block.length < 0x10000) {
block += shellcode;
}
// 喷射 200 次
var heap = new Array(200);
for (var i = 0; i < 200; i++) {
heap[i] = block.substring(0, block.length);
}
};
这里有个关键点:每个喷射块的大小要一致。为什么?因为堆分配器对不同大小的请求会走不同的分配路径。我建议把块大小控制在0x10000(64KB)左右,这个尺寸在大多数浏览器里都能稳定分配。
个人经验:喷射次数不是越多越好。我曾经试过喷射1000次,结果浏览器直接崩溃了——堆内存耗尽。一般来说,200-500次就够用了。具体数值要根据目标环境的内存大小来调。
Heap Feng Shui:精准布局的艺术
堆喷射是「暴力美学」,而堆风水则是「精准手术」。堆风水的目标不是淹没堆,而是精确控制堆上对象的排列顺序,让漏洞触发时,目标对象恰好落在我们想要的位置。
你可能会问:为什么要搞这么复杂?直接喷射不就行了?嗯,这里有个问题——有些漏洞场景下,堆喷射不管用。比如:
- 目标对象大小固定,喷射块无法覆盖
- 堆分配器有隔离机制(比如Chrome的PartitionAlloc)
- 漏洞只能读取或修改特定偏移的数据
这时候,堆风水就派上用场了。
堆风水的核心操作
堆风水说白了就是三步:
- 清理:释放掉堆上已有的对象,制造「空洞」
- 占位:分配我们控制的对象,填进空洞
- 触发:触发漏洞,让程序访问到我们布置好的对象
我画了一张图,帮你理解这个过程:
堆风水的关键技巧
堆风水看着简单,实际操作起来坑不少。我总结几个关键点:
注意:不同堆分配器的行为差异很大。Windows的LFH(Low Fragmentation Heap)和glibc的ptmalloc完全是两套逻辑。写exploit之前,一定要先搞清楚目标用的什么分配器。
技巧1:对齐问题
堆分配器通常会做对齐。比如Windows上,小于16字节的请求会按16字节对齐。这意味着你分配一个17字节的对象,实际占用32字节。我建议在构造恶意对象时,把大小对齐到分配器的粒度上,否则布局会乱。
技巧2:避免合并
释放相邻的空闲块时,分配器可能会把它们合并成一个更大的块。这会导致你的布局被打乱。解决办法是在目标对象之间插入「隔离块」——分配一些不需要释放的小对象,防止合并。
// 堆风水 - 隔离块示例
// 目标:在 ObjA 和 ObjC 之间布置恶意对象
// 1. 分配隔离块
var isolator1 = new Array(10); // 防止与前面合并
var target = new Uint8Array(0x80); // 目标对象
var isolator2 = new Array(10); // 防止与后面合并
// 2. 释放目标对象
// ... 触发漏洞释放 target
// 3. 分配恶意对象,占据 target 的位置
var malicious = new Uint8Array(0x80);
// 填充恶意数据
for (var i = 0; i < 0x80; i++) {
malicious[i] = 0x41;
}
技巧3:大小匹配
恶意对象的大小必须和释放的目标对象完全一致。差一个字节都不行。为什么?因为堆分配器会把相同大小的请求放到同一个空闲链表里。大小不对,就占不到那个位置。
堆喷射 vs 堆风水:怎么选?
我经常被问到这个问题。其实没有绝对的答案,要看场景:
| 对比维度 | 堆喷射 | 堆风水 |
|---|---|---|
| 复杂度 | 低,代码量少 | 高,需要精细控制 |
| 稳定性 | 中等,受堆状态影响 | 高,布局可控 |
| 适用场景 | 浏览器、脚本环境 | 本地漏洞、内核漏洞 |
| 对抗检测 | 容易被检测到 | 隐蔽性更好 |
| 内存消耗 | 高,可能触发OOM | 低,只分配必要对象 |
我个人习惯是:能喷射就喷射,喷不了再搞风水。堆喷射写起来快,调试也方便。但如果你面对的是Chrome的PartitionAlloc或者iOS的kalloc,那堆风水就是必须掌握的技术了。
实战中的避坑指南
我曾经在一个项目里踩过一个坑,分享给你:
我曾经...在分析一个Firefox的堆溢出漏洞时,用了堆喷射,结果每次触发都崩溃在不同地址。查了两天才发现,Firefox的堆分配器在分配大块内存时会走mmap路径,喷射的数据根本不在目标堆区域。后来改用堆风水,精确控制对象布局,问题才解决。
所以,做堆利用开发时,一定要先搞清楚目标的内存分配机制。别一上来就喷,喷错了地方等于白干。
另外,调试堆利用时,建议用windbg或gdb的堆分析命令。比如Windows上的!heap -p -a <address>可以查看堆块信息,Linux上的heap命令可以打印堆布局。这些工具能帮你确认布局是否如预期。
小结
堆喷射和堆风水,是堆漏洞利用的两大支柱。一个靠数量取胜,一个靠精度制胜。理解它们的原理和适用场景,你就能在大多数堆漏洞面前游刃有余。
嗯,这一章就到这里。记住:纸上得来终觉浅,绝知此事要躬行。找个真实的漏洞练练手,比看十遍理论都有用。
公众号:蓝海资料掘金营,微信deep3321