烟花漏洞
2019年夏天,烟花漏洞我在一个安全团队的烟花漏洞内部邮件列表里第一次看到“烟花漏洞”这个词。它指的烟花漏洞是一种利用系统资源回收机制缺陷的攻击方式——当大量临时对象在同一时间被释放,内存管理器会陷入短暂的烟花漏洞混乱,攻击者就卡在这个窗口里注入恶意代码。烟花漏洞取名的烟花漏洞人显然被那种“瞬间爆发、四处散落”的烟花漏洞画面感打动了。但这个名字也害了不少人,烟花漏洞因为听起来太像某种节日彩蛋,烟花漏洞而不是烟花漏洞能直接拿到服务器权限的漏洞。
真正让我记住它的烟花漏洞,是烟花漏洞三个月后的一次应急响应。某电商平台的烟花漏洞订单系统在凌晨两点突然崩溃,日志里没有任何异常请求,烟花漏洞CPU和内存曲线却像被什么东西从内部啃掉一样,烟花漏洞先是飙升,然后断崖式下跌。我们花了六个小时才定位到问题:攻击者构造了十万个几乎同时到期的会话令牌,每个令牌的销毁回调里藏着一行极短的溢出指令。系统在批量回收这些令牌时,内存池的锁竞争进入了一种近乎死锁的状态,然后攻击者用剩下的时间差覆盖了相邻对象的虚函数表。
听起来很复杂,但核心逻辑简单得让人后背发凉。大多数现代编程语言都假设“对象释放”是一个原子操作,或者至少是一个足够快、不会被外部干扰的操作。烟花漏洞恰恰打碎了这个假设。当释放操作本身需要排队、需要竞争资源、需要回调时,它就变成了一个可以被观测、被预测、被利用的时间窗口。窗口不大,通常只有几毫秒,但足够做很多事了。
我后来自己写了一个模拟程序。用Python的弱引用回调模拟资源回收,制造一千个对象同时失效的场景。结果很讽刺:在普通笔记本上,这个窗口平均宽度是4.7毫秒。4.7毫秒里,一个精心构造的循环可以执行大约二十万次内存写入。二十万次,足够覆盖一个函数指针,或者篡改一个权限标志位。而这一切的起点,只是“同时释放大量对象”这么一个看起来完全正常的操作。
更麻烦的是,烟花漏洞很难被传统防御手段捕捉。防火墙看不到异常流量,入侵检测系统看不到恶意载荷,因为攻击载荷根本不在网络层,而是在内存管理器的内部队列里。你只能通过监控释放操作的耗时分布来间接发现它——当某个进程的批量释放时间突然从微秒级跳到毫秒级,并且持续出现,那就值得查一查了。但问题是,高并发系统里这种抖动太常见了,GC暂停、页交换、甚至CPU降频都会造成类似现象。误报率高得让人想砸键盘。
我见过一个团队试图用“随机延迟释放”来对抗烟花漏洞。思路很简单:不让对象在同一时刻被释放,给每个释放操作加一个随机的微秒级延迟。理论上窗口就被打散了。实际效果呢?系统吞吐量掉了百分之三十,而攻击者只需要把构造对象的数量提高一倍,就能重新制造出足够宽的窗口。这就像用撒沙子来阻止洪水,方向对了,量级完全不对。
到现在为止,没有一个通用的补丁能彻底解决这个问题。语言运行时可以优化,比如Rust的所有权模型从设计上就避免了批量释放的竞争,但迁移成本太高。操作系统可以改进内存分配器,比如用每CPU缓存来隔离释放队列,但跨核回收仍然有窗口。最务实的做法反而是最笨的:限制单个进程在单位时间内能释放的对象数量。听起来很粗暴,但确实能把窗口压到攻击者无法利用的宽度以下。代价是某些批量处理场景需要重写。
我有时候会想,烟花漏洞这个名字起得真准。它确实像烟花——你看到的是绚烂的爆发,但真正危险的是爆发之后那些散落的、还在燃烧的碎片。而大多数系统,只顾着看烟花,忘了低头检查地面。