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

发布时间:2026/7/28 18:42:50

堆利用避坑:堆风水的不稳定性与不可控因素 堆利用避坑堆风水的不稳定性与不可控因素一、堆风水跑通了 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), timeout2.0 ) except asyncio.TimeoutError: return HeapLayout(target_offset-1, spray_countspray, hitFalse) except Exception: return HeapLayout(target_offset-1, spray_countspray, hitFalse) # 取最后一次分配的相对偏移作为目标对象落点(占位逻辑) cur offsets[-1] if offsets else -1 return HeapLayout(target_offsetcur, spray_countspray, 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(allocatorfake_alloc, target_offset0x100, runs100) rep await probe.sweep([50, 100, 200]) print(json.dumps(rep, ensure_asciiFalse, indent2))单次跑分配序列带超时避免卡死拖垮整个评估。并发跑多轮统计命中率与落点分布把碰运气量化为成功率与标准差。扫描不同喷射规模找出命中率与代价的最优平衡点避免无脑堆喷射量。评估结果应作为利用稳定性的事前指标。若某环境的命中率低于阈值要么调整喷射策略要么放弃这条利用路径转而寻找更稳定的原语。硬上一个不稳定的利用在实战里只会徒增崩溃暴露。四、堆风水的边界环境敏感、防御演进与稳定性上限堆风水有局限性落地前必须看清几条边界。环境敏感性是最根本的约束。同一份利用在不同 OS 版本、不同补丁级别、不同浏览器构建上表现差异巨大。任何在我的机器上能跑的结论都不能直接外推。利用正式使用前要在目标环境的多版本上回归测试给出明确的成功率范围而非单次结果。防御侧的堆随机化在持续演进。现代操作系统引入了堆基址随机化、分配顺序随机化、空闲块链表随机化等多层手段。每一层都把堆风水的成功率往下压。过去靠固定偏移的利用今天往往需要先做信息泄露才能命中。防御侧的演进意味着攻击侧的利用成本持续上升。稳定性有理论上限。在共享堆的并发环境里绝对稳定的堆风水不可能。攻击者能做的是把成功率拉到可接受范围并用多次重试提高整体命中概率。这意味着利用本身会引入可观测的异常行为大量分配、反复触发同一漏洞路径都是检测信号。防御侧可据此设计检测监控进程内异常的分配模式、反复触发的同一异常、短时间内大量相似对象的分配。这些信号即便不能直接阻断利用也能为后续响应提供线索。把堆风水的必然产生噪声反过来用是防御侧的工程机会。最后要承认堆风水只是利用链里的一环。稳定的堆风水不等于稳定的利用后续 ROP 链、权限提升、清理痕迹任何一环失手都会让整条链断掉。把稳定性评估贯穿整条利用链而非只压在堆风水一段才能给出真实的成功率预期。五、总结堆风水这件事demo 和实战是两码事。环境、并发、随机化三重不可控因素叠加成功率从来不是 100%。所以别追求稳定追求可量化——多轮统计命中率与落点分布扫描不同喷射规模找最优平衡点。命中率太低就换路径别硬上。另外别忘了堆风水必然产生噪声防御侧完全可以反过来利用这一点做检测。堆利用的成熟不是一次打成功而是每次打都知道成功率是多少。

相关新闻