灯光熄灭-BYOVD时代的防御和规避(三)
原创
Symantec
Symantec
网络与信息安全参考
2026年10月29日 08:00
山东
去阅读
QUOTE
这些防护挡的是“执行代码、改写控制流、伪造页表”,而真正要紧的攻击,一样都没做。
—— Symantec Threat Hunter Team
本篇为《熄灯》系列第三篇,承接前两篇对攻击手法的梳理。原文第 25 至 32 页,转向防御一侧:内核加固的边界在哪里,以及真正有效的防线应当建在何处。
01
KERNEL HARDENING
内核加固:一连串防护特性,与它们各自的绕过方式
关键发现
KEY FINDINGS · 关键发现
过去十年间,微软为 Windows 内核加入了一系列防护特性,即便攻击者抵达了系统这个最高特权的部分,也能让破坏变得更难。
这些特性每一项都有已知的绕过方式,但微软并不把这一访问级别上的攻击视为标准的安全漏洞。
这些特性旨在阻止攻击者运行自己的代码、改变操作系统的运行流向,或改写内存映射。
而针对安全软件最常用的那些攻击,以上一件都没做。它们只是改动系统中已有的信息。
因此,内核加固是一项值得部署的保障,却不能指望它挡住现实防御规避中最要紧的攻击。
概述
一旦攻击者抵达内核——Windows 中特权最高的部分——他就几乎可以访问机器上的一切。过去十年左右,微软加入了一系列防护特性,让这种访问对攻击者来说没那么好用。本章考察这些特性,以及为何它们对最要紧的攻击影响有限。
微软自己说得很明确:由一名已经是管理员、且已抵达内核的攻击者所发起的攻击,不被作为标准安全漏洞处理。本章中的这些特性,只是为了让攻击者的日子更难过,并不提供一道有保证的屏障;而且每一顶特性都有一个公开已知的绕过方式。
这些特性各自阻断不同类别的行为:找到攻击者想要找的东西;在内核中安装或运行他们自己的代码;改变操作系统从一条指令流向下一条指令的方式(面向返回的编程);或者触及存放在操作系统另一个隐藏层中的、格外敏感的数据。每一项都封掉了一类否则本可使用的攻击。
问题在于,针对安全软件最常用的那些攻击,一类都不属于上述范畴。攻击者并没有运行新代码,没有改变操作系统的运行流向,也没有改写内存映射——他们只是改动操作系统本来就持有的信息:标明哪些进程受保护的那个标志、告诉内核哪些安全软件希望收到事件通知的那些表项,或者某个运行中代理的权限。这些都不在本章所述防护的拦截范围之内,因为它们都不需要那些防护本来要阻止的行为。
由此得出的实际结论是:内核加固值得部署,因为每项特性都封掉了一些否则可能发生的攻击;但它不能被当作抵御现实攻击者的唯一防线。
深入剖析
一旦攻击者在 Windows 内核中运行起来,操作系统并非毫无防御。过去十年左右,微软在内核自身之上层层叠加了一组内核加固特性。这些特性旨在增加漏洞利用的难度:它们随机化内核的加载地址,限制可执行代码的来源,约束间接函数调用与返回的方式。原则上,它们应当足以拖慢甚至阻止一个已经通过 BYOVD 攻击或本文前述其他技术取得内核访问权的攻击者。
然而在实践中,内核加固最好被理解为一组缓解措施,而非一道硬性的安全边界。值得注意微软如何看待这一整类问题。微软公开发布的 Windows 服务准则明确指出:对 NT 内核而言,“管理员到内核”并不构成安全边界。推而广之,一个篡改 PPL 的管理员,默认情况下也不会被视为在利用漏洞。因此,上文描述的那些技术,最好被理解为对缓解措施的绕过,而非对正式安全边界的利用。微软的 Defender 团队有时确实会在标准的微软安全响应中心(MSRC)流程之外修补它们,但没有 CVE、没有漏洞赏金致谢,时间表也不与 MSRC 同步。MSRC 与 Defender 团队之间的这种割裂,正是这类问题在实际环境中长期存在的原因之一。
— 内核加固的盲区:每项特性挡住什么,以及它从来不是为了拦下什么(原文第 26 页)
结果是,本章所描述的大多数加固特性,其存在意义是让攻击者的日子更难过,而不是让漏洞利用变得不可能。每一项都有已公开的绕过方式;而一旦攻击者在内核中拥有了可靠的读写原语,这些绕过方式往往看起来非常相似:绕开防护而不是正面硬碰的纯数据攻击。
内核地址空间布局随机化(KASLR)
KASLR 是最基础的加固特性。Windows 内核及其模块在每次启动时都加载到不同的基址。有了这项特性,一个知道自己开发机上某个目标结构位于何处的攻击者,无从得知它在目标机器上的位置。如果攻击者只有写入原语却算不出该往哪里写,就无法可靠地篡改所需的内核数据结构;而一旦写错地方,机器很可能崩溃。
KASLR 使两台机器的内核恰好布局在同一地址的可能性变得极小。面对一个只有有限写入原语、又无法得知内核布局的攻击者,KASLR 可以是一道有意义的障碍。
然而,绕过方式早已被摸透。任何以 SeDebugPrivilege 运行的进程,都可以用 SystemModuleInformation 类调用 NtQuerySystemInformation,取回一份机器上已加载的全部内核模块及其基址的列表。管理员通常就拥有 SeDebugPrivilege,而在某些 Windows 版本与配置下,连非管理员用户也能取得它。一旦攻击者拿到所关心内核模块的基址,就能算出其中任何结构的地址,KASLR 便不再提供任何保护。
诸如 EDRSandblast 与 RealBlindingEDR 之类的公开工具就采用了这一思路。一旦攻击者取得管理员级访问权,KASLR 就不再是一道有意义的障碍。
基于虚拟化的安全与虚拟机监控程序保护的代码完整性
基于虚拟化的安全(Virtualization-Based Security,VBS)与虚拟机监控程序保护的代码完整性(Hypervisor-Protected Code Integrity,HVCI)是一项更为实质的缓解措施。在受支持的硬件上,VBS 引入一个安全内核,与传统的 NT 内核一同运行在虚拟机监控程序之下。对某些操作而言,传统内核实际上变成了安全内核的客户端。部分敏感数据被整体移出传统内核,使一个在传统内核中以管理员、甚至以内核特权运行的攻击者也无法触及。
最清楚的例子是 Credential Guard。在 VBS 出现之前,LSASS 把凭据材料保存在自己的地址空间中,一旦 Mimikatz 这类工具取得管理员访问权,就能读取并解密它。启用 Credential Guard 后,凭据材料由安全内核管理,从传统内核再也无法抵达。一个 Mimikatz 式、遍历 LSASS 地址空间的攻击,只会发现空无一物。在微软看来,这是少数几个真正被当作安全边界对待的“管理员到内核”场景之一,因为安全内核被认为与传统内核相互独立。
HVCI 对传统内核自身施加了一条独立的不变式:可执行的页不能同时可写。即便攻击者已经取得物理内存写入权限——例如通过前文所述的 GodFault 原语——他们也无法直接改写内核代码。任何对存放可执行指令的页的写入尝试都会被拒绝。这迫使攻击者把自己限制在数据篡改上,而无法去打代码补丁。
原则上,HVCI 是一项强有力的缓解措施。现实中,它挡不住针对安全软件最要紧的那些攻击,因为其中大多数本就是纯数据的。从内核的通知回调数组中删除条目、翻转 EPROCESS 结构的Protection 字段、摘掉一条回调链、或解除安全进程内部的内存映射——这些都是对数据的写操作,而非对代码的写操作。HVCI 对它们一概不作限制。只要攻击者只需要读写数据结构,HVCI 对他们而言基本是透明的。
也有一些更高级的技术,专门用于在攻击者确实想调用内核代码的情况下击破 HVCI。KernelForge 是一个公开研究项目,它通过精心安排栈帧,让 Windows 内核自己替攻击者执行所需的那串 API 调用。加载一个未签名驱动——这通常会被代码完整性强制拦下——正是该技术支持的操作之一。KernelForge 把 HVCI 从一道硬屏障变成了又一个可以绕开的障碍,但门槛相比简单的纯数据攻击确实高得多。
内核控制流保护与英特尔 CET
内核控制流保护(Kernel Control Flow Guard,kCFG)是微软针对内核中控制流劫持攻击的缓解措施。这类攻击的经典例子是面向返回的编程(ROP):攻击者通过操纵返回地址或函数指针,把已有代码的短小片段(称为 gadget)串接起来。kCFG 在间接调用点插入检查,使内核只能调用一个已知有效目标集合内的地址。一个把函数指针装进寄存器、试图跳进某个子程序中段的攻击者会被拦下。
然而,kCFG 并不是对 ROP 的完整防御。Windows 内核包含数以兆字节计的代码,有效调用目标的集合足够大,只要有足够时间,攻击者仍能找到一条可用的链。不过,它确实收窄了 gadget 集合,让漏洞利用变慢、也变得更脆弱。
英特尔的控制流强制技术(Control-flow Enforcement Technology,CET),尤其是其中的间接分支跟踪(Indirect Branch Tracking)特性,比 kCFG 明显更强。它借助硬件支持来强制要求:间接分支只能落在被明确标记为有效目标的指令上。但据我们所知,间接分支跟踪目前尚未内建于 Windows 内核,即便在支持它的 CPU 上也是如此。在微软选择实现它之前,kCFG 仍是相关的内核缓解措施。
与其他加固特性一样,kCFG 与 CET 只在攻击者试图改变内核中的控制流时才有意义。纯数据攻击完全绕过它们。公开研究也揭示了这一缺口:一份 2024 年的 HN Security 报告描述了 Windows 11 中的一个任意指针解引用如何被转化为任意读写原语,全程无需绕过 kCFG,仅靠与数据打交道即可。
内核模式硬件强制堆栈保护
内核模式硬件强制堆栈保护通过保护调用序列的另一端,与 kCFG 形成互补。kCFG 关注的是间接调用指令,堆栈保护关注的则是返回。这项缓解措施为返回地址维护一个独立的影子栈,在有硬件支持的地方由硬件维护。当函数返回时,它从常规栈上弹出的返回地址,会与影子栈上对应的条目进行比对;若不一致,返回即被阻止。这让面向返回的编程显著变难,因为攻击者再也不能简单地覆盖栈上的返回地址来改变控制流。
绕过的故事到这里已经很熟悉了。纯数据攻击完全无视堆栈保护,因为它们并不改变控制流。而对于确实需要改变控制流的攻击,也有更高级的利用技术可用。
一个例子是 keyjumper,一项公开研究技术,演示了在特定环境下绕过影子栈强制的方法。有的攻击者在需要一个绕过手段时会动用 keyjumper,也有人在情况允许时偏好更简单的路径。无论哪种,模式都一样:堆栈保护让日子更难过,但只要对寄存器或内存有足够的控制力,攻击者通常仍能抵达目标。
虚拟机监控程序强制分页转换
虚拟机监控程序强制分页转换(Hypervisor-Enforced Paging Translation,HVPT)是一项更近期的缓解措施,它借助虚拟机监控程序来校验传统内核中的页表修改。其目标是阻止已经取得内核写入原语的攻击者,通过伪造或篡改页表项来重映射内存,从而击破其他缓解措施。
在针对安全软件的攻击语境中,HVPT 不太可能带来实质差别。我们在实际环境中看到的大多数实用攻击,并不需要伪造可分页的页表项。它们篡改的是 EPROCESS 的保护字节、内核通知数组,或进程令牌,而 HVPT 并不限制这些操作。这项缓解措施值得欢迎,但它与最常见的绕过路径还有一段距离。
共同线索:纯数据攻击
把各项加固特性作为一个整体来看,一个清晰的模式浮现出来。每一项都是为了增加某一特定类别漏洞利用的难度:KASLR 让目标更难被找到;HVCI 让代码更难被打补丁;kCFG 与 CET 让间接调用更难被改变流向;堆栈保护让返回更难被改变流向;HVPT 让页表更难被伪造。
它们的共同点是,都假定攻击者必须做成其中一件事才能得手。而对于一个目标就是让安全软件失效的攻击者来说,这个假定大体上是错的。针对安全软件的决定性操作——移除内核回调、翻转 EPROCESS 结构上的保护字节、剥夺进程令牌的权限、解除受保护进程内部的内存映射——无一不是通过对数据结构的写操作实现的。它们都不需要在内核中执行攻击者代码,都不需要改变控制流,也都不需要伪造页表。
一旦攻击者在内核中拥有了可靠的读写原语——通常来自一个易受攻击的已签名驱动——内核加固对这类攻击提供的保护就很有限了。加固仍然可以拖慢或阻止那些需要执行代码、劫持控制流、或操纵页表的攻击。但对于现实中的绝大多数防御规避,攻击者只是改动他们需要改的数据,然后整个绕开加固。
这并不是反对内核加固的论调。这些缓解措施每一项都封掉了一类否则可能发生的攻击,合在一起,它们限制了攻击者的选项。但这确实是一个不该只依赖内核加固的理由。防御方不应假定,一个已经抵达内核模式的攻击者会被内核自身的防护挡住。面对这些障碍,他们只会转而采用纯数据手法。
02
DEFENDING AGAINST BYOVD
防御 BYOVD
尽管存在多种防御规避技术,BYOVD 仍在整个图景中占据主导。对防御方而言,自然的问题是:什么能帮助抵御它。当今对 BYOVD 的应对由两种防御思路主导,而两者都有各自的局限。
第一种是微软的易受攻击驱动阻止列表(Vulnerable Driver Blocklist)。当一个易受攻击的驱动被上报后,微软会与发布方合作解决问题,并把该驱动加入阻止列表,使保持更新的 Windows 安装拒绝加载它。这确实有用,但有两个问题。第一是延迟:从一个驱动被认定为易受攻击,到阻止列表更新抵达企业端点,中间有数天、更多时候是数周的滞后。在这段窗口期内,该驱动可以被自由用于攻击。第二是数量:已有数百个已知的易受攻击驱动在流通,而任一时刻被列入阻止列表的只是其中一部分。防御方封掉一个,攻击者换下一个就是了。
第二种思路是安全软件基于签名的检测。当研究者分析某个特定的 BYOVD 工具时,他们会计算一个哈希值并为该文件编写一条签名。这条签名会标记该工具今后的任何一次出现。这确实有效,但有效期很短。许多 BYOVD 工具起初是公共代码分享平台上的开源概念验证项目。当原始项目被标记后,攻击者会在几天之内重新实现它,往往换一种语言。我们常常看到同一底层技术接连被用 C、C++、C#、Rust 和 Go 快速重新实现。每一次换语言,都击破了现有的哈希签名。而加密器与打包器让问题雪上加霜:即便底层逻辑没变,二进制也会发生变异。
BYOVD 工具如今常常被打包进勒索软件即服务套件,这让附属成员获得了源源不断的新变体,背后还有运营支持。
这类防御的问题在于,它们在本质上纯粹是被动反应式的。每一个新变体都必须先在实际环境中被观察到,才可能被防御;而等防御方做出反应,攻击者早已转向别处。
主动防御,而非被动响应
真正能改变平衡的思路,是去监控文件在与内核驱动交互时做了什么,而不只是监控加载了哪些驱动。无论一个 BYOVD 工具是一年前写就的 C++ 项目,还是上周刚写的 Rust 移植版,其操作模式大体相同:一个易受攻击的驱动被投放到磁盘上;创建一个服务并启动它来加载该驱动;随后恶意组件枚举运行中的进程,识别它关心的目标,并向驱动发送一条特定的输入/输出控制(IOCTL)命令,指示它对那些目标采取某项特权操作。
盯住这个模式,而不是盯住某个具体驱动或加载它的具体二进制文件,就能让防御方获得一个独立于表面细节的观察点。赛门铁克与 Carbon Black 的端点产品如今已包含针对这类驱动交互的行为监控。这套检测逻辑会标记针对内核驱动的异常 IOCTL 流量,包括针对安全软件的进程终止请求、会让安全进程崩溃的句柄剥夺,以及会让 EDR 失明的回调移除。
这套组合检测逻辑在两个层面上运作:
在更具体的层面上,我们维护一份已知易受攻击驱动的目录,以及这些驱动为执行敏感操作所接受的 IOCTL 代码。当目录中的某个驱动被观察到收到一条敏感 IOCTL 时,该请求会被检查。如果目标是已知与安全相关的进程,活动即被阻止。这份目录覆盖广泛,并随着新的易受攻击驱动被识别和分析而不断扩充。
在更通用的层面上,我们认识到:无论请求的是哪个驱动,一条终止运行中安全软件进程的请求都是异常的。通过跟踪端点上安全软件的进程标识符,系统可以检测到针对这些标识符之一发起 IOCTL 的尝试,即便被滥用的驱动是一个此前从未见过的驱动。正是这个通用层面,让一个未知的易受攻击驱动在它第一次出现在实际环境中、尚未被加入任何目录或阻止列表之前就被捕捉到。
为什么行为监控能捕捉到静态防御漏掉的东西
有三个特性,使行为监控与它所补充的那些防御思路产生了实质区别。
它与驱动无关。 检测的通用层面会标记针对安全软件进程标识符的敏感 IOCTL,无论涉及哪个驱动。攻击者无法仅靠换用一个更冷门的易受攻击驱动来击破这层防护,因为这项检测并不依赖事先识别出具体驱动。
它不受微软发布节奏的约束。 易受攻击驱动阻止列表是间歇性更新的,通常随 Windows 的重大版本发布。而行为防护通过安全软件的更新通道下发,这使我们能够对不断变化的威胁快速做出反应。一个新出现的易受攻击驱动,可以在数小时内——而不是数周或数月——被行为防护覆盖。
它的误报风险低。 合法的内核驱动有可预期的行为特征:它们为服务某个特定产品而构建,只接受来自一组已知客户端的已知命令集,正常使用中不会按需终止安全软件。阻断恶意的行为模式,不会影响合法驱动的正常使用。
净效果是,自 BYOVD 流行以来攻击者所享有的时间优势,开始收窄。一个新的易受攻击驱动、一个用新语言写的新加载器、一个施加在旧加载器上的新打包器:这些单独任何一项都无法击破行为防护,因为它们都没有改变行为防护所盯的那个操作模式。
更广泛的启示
BYOVD 是当今实际环境中最主要的防御规避技术,但它属于一个更长的趋势。随着微软与安全软件行业不断加固用户模式与内核模式的接口,攻击者转而滥用操作系统自身所信任的那部分特权代码。同样的转向,在受保护进程篡改、在让 EDR 云服务失联、以及在所有这些技术于勒索软件即服务产品中逐步工业化,都能看到。
防御上的应对,不能只限于为每一个新变体编写签名,或等待每一个新的易受攻击驱动被列入阻止列表。签名加阻止列表的做法能争取时间,但它能争取到的时间正在缩短。除此之外还需要做的,是持续关注那些无论攻击者拿起哪种工具都必须实施的行为。
END
原文作者:Symantec Threat Hunter Team。译文仅供研究参考,术语与数据以英文原版为准。
《熄灯》三篇连载到此结束。如果你觉得这一系列有收获,欢迎点赞、在看、转发三连。
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。









评论