堆利用避坑:堆风水的不稳定性与不可控因素

📅 2026/7/28 18:58:18 👁️ 阅读次数 📝 编程学习
堆利用避坑:堆风水的不稳定性与不可控因素

堆利用避坑:堆风水的不稳定性与不可控因素

一、堆风水跑通了 demo,实战却打不稳

堆风水与堆喷射是浏览器与办公软件漏洞利用的经典手法。思路是:通过反复分配和释放特定大小的对象,把堆布局"梳"成攻击者想要的样子,让敏感对象与可控数据落在可预测的相对位置。这套手法在 demo 里能稳定触发,实战却经常失手。

环境差异是第一关。同一份利用代码,在不同 Windows 版本、不同浏览器构建上跑,堆布局完全不同。底层堆分配器的实现细节随版本调整,freelist 的回收顺序、相邻块的合并策略、元数据的存放位置都会变。在 Win10 上跑通的利用,到 Win11 可能十次九次失败。

并发是第二关。现代浏览器是多进程多线程,堆是共享资源。利用代码试图把堆梳成特定布局,但同一时刻还有其他线程在做完全相反的分配释放。即便你精确控制自己的分配序列,也无法阻止后台线程插入任意操作,让精心构造的布局被一两次意外分配打乱。

ASLR 与堆随机化是第三关。堆本身的基址被随机化,分配顺序也有一定随机性。攻击者要么靠信息泄露拿到基址,要么靠大量喷射制造"总会命中"的概率。前者依赖额外的泄露原语,后者用空间换确定性,但成功率仍受堆碎片化影响。

更麻烦的是"调试器陷阱"。在调试器下跑利用,堆行为可能与正常执行不同,因为调试器会改变进程的某些路径与时机。开发者以为利用已经稳定,离开调试器就立刻失败。这个坑我踩过不止一次,调试器下的"稳定"和真实环境下的"稳定"是两回事。

二、堆布局的不可控链路:从分配序列到最终位置

把堆风水的链路拆开看,攻击者能控制的只是分配序列,最终位置由多个不可控因素共同决定。

攻击者能控制的是"我分配多少个、什么大小、什么顺序"。但落点决定权在分配器手里。freelist 的回收顺序决定了哪个空闲块被优先使用,相邻块的合并与分裂决定了真实占用大小,这些都是分配器内部策略,攻击者只能间接推测。

并发线程是另一层噪声。同一进程里的其他线程也在分配释放,它们插入的任何一次操作都可能占用攻击者瞄准的空闲块,让后续分配落到完全不同的位置。堆风水里反复出现的"喷一波再打",本质就是用数量对抗这种不确定性,但不能消除。

ASLR 与堆随机化进一步抬高门槛。基址随机化让绝对地址不可预测,分配顺序的随机化让相对位置也不完全可控。攻击者要么先做信息泄露,要么用大规模喷射制造概率命中。前者依赖额外漏洞,后者成功率受堆状态影响,从来不是百分之百。

最终的成功率是环境、并发、随机化三者叠加的结果。任何一项变化,都可能让原本稳定的利用退化到不可用。这就是堆风水最本质的工程难题:稳定性比单次成功更难。

三、堆布局稳定性评估:把"碰运气"变成可量化的成功率

下面是一段堆布局稳定性评估的骨架。它在目标环境里反复跑分配序列,统计目标对象的落点分布,给出可量化的成功率,避免只凭"试一次成功"下结论:

import asyncio import json import random import statistics from dataclasses import dataclass @dataclass class HeapLayout: target_offset: int # 目标对象相对基准的偏移 spray_count: int # 喷射次数 hit: bool # 本次是否命中预期位置 class HeapFengShuiProbe: def __init__(self, allocator, target_offset: int, runs: int = 200): self._alloc = allocator self._target = target_offset self._runs = runs async def _one_run(self, spray: int) -> HeapLayout: # 单次跑分配序列,模拟堆风水,记录目标对象落点 try: offsets = await asyncio.wait_for( self._alloc(spray), timeout=2.0 ) except asyncio.TimeoutError: return HeapLayout(target_offset=-1, spray_count=spray, hit=False) except Exception: return HeapLayout(target_offset=-1, spray_count=spray, hit=False) # 取最后一次分配的相对偏移作为目标对象落点(占位逻辑) cur = offsets[-1] if offsets else -1 return HeapLayout(target_offset=cur, spray_count=spray, hit=(cur == self._target)) async def measure(self, spray: int) -> dict: # 并发跑多轮,统计命中率与落点分布,避免单次结果误导 sem = asyncio.Semaphore(16) async def _one(): async with sem: return await self._one_run(spray) results = await asyncio.gather(*[_one() for _ in range(self._runs)]) hits = sum(1 for r in results if r.hit) offsets = [r.target_offset for r in results if r.target_offset >= 0] return { "spray_count": spray, "runs": self._runs, "hit_rate": round(hits / self._runs, 4), "offset_mean": statistics.mean(offsets) if offsets else 0, "offset_stdev": statistics.pstdev(offsets) if offsets else 0, } async def sweep(self, sprays: list[int]) -> list[dict]: # 扫描不同喷射规模,找出命中率与代价的最优平衡点 return await asyncio.gather(*[self.measure(s) for s in sprays]) # 使用示例(伪分配器,真实环境替换为目标进程内的分配探针) async def demo(): async def fake_alloc(n): await asyncio.sleep(0.001) # 模拟:大多数情况下落在固定偏移,少数情况下被并发打乱 cur = 0x100 if random.random() > 0.2 else 0x200 return [0x100] * (n - 1) + [cur] probe = HeapFengShuiProbe(allocator=fake_alloc, target_offset=0x100, runs=100) rep = await probe.sweep([50, 100, 200]) print(json.dumps(rep, ensure_ascii=False, indent=2))

单次跑分配序列带超时,避免卡死拖垮整个评估。并发跑多轮统计命中率与落点分布,把"碰运气"量化为成功率与标准差。扫描不同喷射规模,找出命中率与代价的最优平衡点,避免无脑堆喷射量。

评估结果应作为利用稳定性的事前指标。若某环境的命中率低于阈值,要么调整喷射策略,要么放弃这条利用路径,转而寻找更稳定的原语。硬上一个不稳定的利用,在实战里只会徒增崩溃暴露。

四、堆风水的边界:环境敏感、防御演进与稳定性上限

堆风水有局限性,落地前必须看清几条边界。

环境敏感性是最根本的约束。同一份利用在不同 OS 版本、不同补丁级别、不同浏览器构建上表现差异巨大。任何"在我的机器上能跑"的结论都不能直接外推。利用正式使用前,要在目标环境的多版本上回归测试,给出明确的成功率范围,而非单次结果。

防御侧的堆随机化在持续演进。现代操作系统引入了堆基址随机化、分配顺序随机化、空闲块链表随机化等多层手段。每一层都把堆风水的成功率往下压。过去靠固定偏移的利用,今天往往需要先做信息泄露才能命中。防御侧的演进意味着攻击侧的利用成本持续上升。

稳定性有理论上限。在共享堆的并发环境里,绝对稳定的堆风水不可能。攻击者能做的是把成功率拉到可接受范围,并用多次重试提高整体命中概率。这意味着利用本身会引入可观测的异常行为,大量分配、反复触发同一漏洞路径,都是检测信号。

防御侧可据此设计检测:监控进程内异常的分配模式、反复触发的同一异常、短时间内大量相似对象的分配。这些信号即便不能直接阻断利用,也能为后续响应提供线索。把堆风水的"必然产生噪声"反过来用,是防御侧的工程机会。

最后要承认,堆风水只是利用链里的一环。稳定的堆风水不等于稳定的利用,后续 ROP 链、权限提升、清理痕迹任何一环失手都会让整条链断掉。把稳定性评估贯穿整条利用链,而非只压在堆风水一段,才能给出真实的成功率预期。

五、总结

堆风水这件事,demo 和实战是两码事。环境、并发、随机化,三重不可控因素叠加,成功率从来不是 100%。所以别追求"稳定",追求"可量化"——多轮统计命中率与落点分布,扫描不同喷射规模找最优平衡点。命中率太低就换路径,别硬上。另外别忘了,堆风水必然产生噪声,防御侧完全可以反过来利用这一点做检测。堆利用的成熟,不是一次打成功,而是每次打都知道成功率是多少。