文章总结: 本文探讨了在启用HVCI与PatchGuard的现代Windows系统中,通过数据面操作实现进程隐藏的技术。核心方案是利用Pssetcreateprocessnotifyroutineex注册的进程终止回调,在Pspprocessdelete执行链表完整性检查前的微秒级窗口内,临时修复被篡改的ActiveProcessLinks链表指针,从而绕过传统PatchGuard的校验,实现进程生命周期的隐藏与干净退出。该方法完全基于VTL0数据面操作,不触及受EPT保护的代码页,但受限于内核执行权限,且面临VTL1中SKPG未知监控的残余风险。 综合评分: 90 文章分类: 恶意软件,免杀,内核安全,逆向分析,红队
在HVCI与PatchGuard夹缝中操纵进程链表的微秒对决
原创
Noair Noair
Ghost Wolf Lab
2026年5月12日 16:22 北京
在小说阅读器读本章
去阅读
摘要
Windows基于虚拟化的安全架构已将从内核发起的代码篡改从“软件对抗”彻底升格为“硬件拒绝”。过去通过DKOM从 ActiveProcessLinks 摘除进程的经典隐藏手法,如今在传统PatchGuard周期性验证与Hypervisor级代码完整性(HVCI)的二阶段地址转换钳制下几无生存空间。
本文从Intel VT-x的EPT(扩展页表)权限叠加模型切入,逐层还原写入内核代码页被硬件截杀的精确微架构序列,并在此基础上,剖析一种完全沉在VTL0数据面、不触碰任何代码页的极端窄路:利用 PsSetCreateProcessNotifyRoutineEx 注册的终止回调,在 PspProcessDelete 发起链表完整性检查前一个微秒级窗口内,暂态修复被篡改的 LIST_ENTRY 结构,换取进程整个生命周期的隐身与一次干净的退出。同时,本文将在SKPG的未知监控阴影下,严格框定该方法的前提、局限及旁支理论路径的不可能性。
内核钩子结构性
在Windows 7/x64时代,攻击者尚可通过将CR0.WP置零来让CUP忽略页表的写保护位,或直接修改PTE的 W 位后再行内联挂钩,从而在PatchGuard的扫描间隔中安然存活。这些技法的共同前提是:防御逻辑与攻击代码运行在同一特权级——只要理解了PatchGuard的校验周期与隐蔽手法,就能以更隐蔽的代码替换它的代码。
HVCI的到来宣告了这种对等博弈的死亡。微软不再企图让VTL0内的软件防范一个拥有Ring-0权限的攻击者,而是将整个防御体系迁入一个高于Ring-0、独立于正常Windows执行环境的特权维度——由Hypervisor与安全内核(VTL1)控制的虚拟机监控层。
研究的核心目标由此收束成一个具体且可重复验证的命题:在已启用HVCI与VBS的现代Windows系统上,能否仅通过VTL0内核数据面的精确操作,让从 ActiveProcessLinks 中摘除的进程在终止时不触发 0x139 KERNEL_SECURITY_CHECK_FAILURE 蓝屏。 我们寻找的不是又一个颠覆架构的漏洞,而是在架构已经划定的铜墙铁壁之下,寻找密度最高的那一丝数据面微缝。
VTL隔离到EPT逐级钳制
在探讨任何攻击向量之前,必须先完整建模挡在我们面前的三层硬件级防御栈。这并非简单的“一堵墙”,而是一个层层叠加、逐级降权的多维度执行控制体系。
启用VBS后,Windows实质上成为一台寄生于Hyper-V的虚拟机。CPU的VT-x/AMD-V扩展在硬件层面拆出两个平行但权限不对等的执行世界:
- VTL0(正常世界):承载完整的Windows内核镜像、所有第三方驱动程序与全部用户态进程。它被视为不可信实体,其页表、寄存器上下文与内存区域均受Hypervisor的持续监视。
- VTL1(安全世界):运行一个极度精简的专用内核(securekernel.exe),其唯一职责就是执行代码完整性策略——CI.dll 与 SKCI.dll 托管于此。VTL1拥有对VTL0内存的单向读权限,但自身内存对VTL0完全不可见、不可访问。
两界之间唯一的通信信道是由Hypervisor仲裁的Hypercall机制。VTL0可以向VTL1发送经过严格格式化与校验的调用请求,而VTL1完全自主决定是否响应。这意味着,即使攻击者已完整控制VTL0的Ring-0,也无法读取或污染驻留在VTL1中的完整性验证逻辑。
如果说VTL是宏观的空间隔离,那么EPT(扩展页表)就是对内核代码页进行“不允许”判断的硬件闸刀。理解其运行细节,是认清所有传统钩子手法失败根源的关键。
客户页表转换(VTL0视角)
当内核模块尝试向 nt!PspProcessDelete 所在虚拟地址 0xFFFFF80274EE1310 写入一个 0xCC(INT3操作码)时,CPU首先信任地走过Windows自身维护的页表层级:
CR3 → PML4 → PDPT → PD → PT → PTE
攻击者当然可以提前修改目标PTE,将其 W 位置1,甚至将 CR0.WP 清零以让CPU彻底忽略该位。这一级转换顺利产出客户物理地址(GPA) 0x274EE1000,并从Windows视角宣告“写入合法”。
EPT权限叠加与硬件违规
危险就在下一步。
CPU取得GPA后,并不会直接送往内存总线,而是强制将其送入第二级页表——由Hypervisor维护的EPT进行再次转换。
#include <stdint.h>
// EPT 页表项定义
typedef union _EPT_PTE {
uint64_t All; // 原始64位数值
struct {
uint64_t Read : 1; // Bit 0: 可读
uint64_t Write : 1; // Bit 1: 可写
uint64_t Execute : 1; // Bit 2: 可执行
uint64_t EptMemoryType : 3; // Bit 5:3: EPT内存类型
uint64_t IgnorePAT : 1; // Bit 6: 忽略PAT
uint64_t Ignored1 : 1; // Bit 7: 忽略
uint64_t UserModeExecute : 1; // Bit 8: 用户模式执行
uint64_t Ignored2 : 3; // Bit 11:9: 忽略
uint64_t PageFrameNumber : 40; // Bit 51:12: 物理页帧号 (Page Frame Number)
uint64_t Reserved1 : 12; // Bit 63:52: 保留
} Fields;
} EPT_PTE, *PEPT_PTE;
EPT结构对VTL0完全不可达,且格式与客户PTE相同,但权限位独立决定:
EPT_PML4 → EPT_PDPT → EPT_PD → EPT_PT → EPT_PTE
对于所有内核代码页,Hypervisor在此处的EPT_PTE中强制设置: R=1,W=0,E=1。
当CPU发现来自客户端的写入请求与EPT的W=0冲突时,硬件立即停止当前指令流,生成VM Exit,退出原因为 EPT_VIOLATION。此时,VTL0的页表设置、CR0.WP的值、乃至整个Windows的意见都毫无意义——EPT的否决是CPU指令退休之前最后一道不可绕过的硬件判决。
VM Exit到VTL1的错误检查链
VM Exit发生瞬间,CPU保存VTL0的完整状态,切换至Hypervisor上下文。Hypervisor解析退出原因,提取触发违规的GPA(0x274EE1000)、操作类型(写入)与原始虚拟地址,识别其位于内核代码区,随即判定为代码完整性违规,并将事件立即转发至VTL1。VTL1安全内核接收事件后,不进行任何恢复或协商,直接调用 KeBugCheckEx,触发系统崩溃。
这四阶段链条——客户页表允许 → EPT拒绝 → VM Exit → VTL1 BugCheck——是理解整个现代Windows攻击面约束的基石。它从根本上定义了一件事:任何修改内核代码页的尝试,最终都会被CPU硬件本身拦截,且在VTL0内无法干扰此拦截链的任何一环。
在此之上,微软布设的并非单一防线,而是三个独立且互保的监控层:
- VTL0传统PatchGuard:仍运行在原内核上下文中,周期性校验关键数据结构(如
ActiveProcessLinks)。因其与攻击代码同处VTL0,存在被绕过或欺骗的可能性,但依旧构成第一道噪音屏障。 - **VTL1 HVCI **(SKCI):正是上文所述依托EPT的硬件级代码完整性强制。它使内核
.text段成为绝对只读只执行区域,从物理上关闭了挂钩时代。 - VTL1 HyperGuard (SKPG):可视为对安全内核自身的PatchGuard。它持续校验securekernel.exe镜像、VTL1数据结构、EPT基址与VMCS完整性,确保即使VTL0已沦陷,攻击者也无法通过破坏VTL1来关闭HVCI。
我们的攻击面由此被严格框定:只能操纵VTL0内的可写数据面,只能欺骗运行于VTL0的传统PatchGuard,而对VTL1的HVCI与SKPG毫无干扰手段——甚至连后者的检查范围都无从探知。
进程隐藏与PatchGuard判定
EPROCESS结构中嵌入的 ActiveProcessLinks 是一个标准的 LIST_ENTRY 双向链表,将系统内所有活跃进程串接。任务管理器、Process Hacker等枚举工具正是通过遍历此链表来构建进程列表。
经典DKOM隐藏的操作就是修改前后邻居的 Flink/Blink 指针,使目标进程脱离链表环。进程仍然生存并可被调度,但已从常规枚举器中消失——直至它终止的那一刻。
内核函数 PspProcessDelete 在清理终止进程时,会执行严格的链表完整性校验:检查 Flink->Blink 与 Blink->Flink 是否都指向当前条目。如果指针因隐藏操作而悬空或错位,函数将执行 int 29h 快速失败中断,径直走向 0x139 KERNEL_SECURITY_CHECK_FAILURE 蓝屏。该中断位于 nt!PspProcessDelete+0x95——它不是可返回的错误码,而是立即终结执行流的判决指令。
更致命的是,PspProcessDelete 的整个校验与 int 29h 逻辑驻留在内核 .text 段中,受EPT的R/E保护。我们无法通过修改 jz 为 jmp 来跳过它——任何写入尝试都将被前述四阶段链截杀。
攻击路径
问题的形状因此变得异常明确:不触碰受影响代码页,仅在PatchGuard检查运行前,将其将要观察的数据面修复至合法状态,待检查通过后进程已然终止,无需再回归隐藏态。
考察进程终止到PspProcessDelete执行之间的调用链,可以发现一个完美的介入点:PsSetCreateProcessNotifyRoutineEx注册的回调函数。该回调在进程创建或终止时被通知,其进程终止路径恰在链表校验逻辑投运之前的极短窗口内被调用。
回调原型接收三个参数:
void NotifyCallback(
PEPROCESS Process,
HANDLE ProcessId,
PPS_CREATE_NOTIFY_INFO CreateInfo
);
当 CreateInfo == NULL 时,即表示进程终止事件。此时 Process 指向的 EPROCESS 结构内部,ActiveProcessLinks 仍保持着被篡改的状态——但PatchGuard的检查尚未启动。
在回调上下文中,代码可以安全执行以下数据面操纵(均在可写内存中完成,不触发EPT违规):
- 获取当前链表条目:PLIST_ENTRY OurEntry = &Process->ActiveProcessLinks;
- 提取前后邻居指针:Flink = OurEntry->Flink; Blink = OurEntry->Blink;
- 检测篡改:对比 Flink->Blink 与 Blink->Flink 是否均等于 OurEntry。不等即判定已被摘除。
- 临时修复:将邻居指针恢复指向本条目——Flink->Blink = OurEntry; Blink->Flink = OurEntry;
- 立即返回,让控制流回到内核终止路径。
此刻,PspProcessDelete 接管的是一份完全合法的链表结构,其 Flink->Blink与Blink->Flink双向一致,校验通过,jz 跳转成功,int 29h 永远不会被触发。进程在它的整个可见生命周期内始终保持隐藏,仅在终止的这几微秒内被临时“拉回”链表以完成合规的销毁——随即连同它的 EPROCESS 一起被系统回收,不再留下任何需要持续隐藏的实体。
前提约束与残余风险
这项技术并非万能解药,其生效受限于一系列刚性前提,且被更高层级的未知监视所笼罩。
内核代码执行权限:操作本身要求代码运行在Ring-0。在现代Windows上,这意味着必须持有受信任的代码签名证书,或通过一个已知的有漏洞的已签名驱动程序代理执行。测试签名仅在开发/调试模式下可用。
仅绕过传统PatchGuard的一项检查:该方法只针对VTL0内传统PatchGuard对 ActiveProcessLinks 的单点校验,且仅处理进程退出时刻的检查。进程存活期间,PatchGuard若在其他时间点发起对同一链表的扫描,可能发现损坏并触发其他错误,但当前实验中尚未观察到。
SKPG仍是黑盒:安全内核补丁卫士运行在VTL1,享有高于VTL0的权限,可以读取VTL0内存。其检查频率、覆盖范围、对链表数据面的监控强度均未被公开。基于VTL0时间窗口的数据面修复对传统PatchGuard有效,但可能完全暴露于VTL1的持续监控之下。这是整个方案最大的不可控变量。
不绕过HVCI:整个过程中,没有任何试图写入代码页的操作。方案的有效性恰恰反证了HVCI代码完整性保护的完备——它迫使攻击者只能在数据面寻找极限微操作。
旁支路径
研究过程中,以下理论向量因硬件或架构级缓解而几乎无法稳定实施:
- 数据段函数指针劫持/虚函数表修改:需定位极稀有的、位于可写内存且不被CFG(控制流保护)覆盖的间接调用目标;即使找到,Kernel CFG在间接调用前会验证地址的有效签名边界,可拦截绝大多数篡改。
- 回调数组直接替换:
PspCreateProcessNotifyRoutine数组虽位于可写内存,但不导出,需运行时模式扫描定位;且Kernel CFG在调用回调前同样会进行地址校验。 - 上下文操纵(线程陷阱帧篡改):在系统调用、APC或线程切换时修改保存的RIP,以类似ROP的方式跳过校验。概念上可行,但面临Intel CET影子堆栈对返回地址的硬件级验证,且对时序的依赖极其脆弱,若目标函数直接调用BugCheck而不返回,则单纯的RIP修改无效。
这些路径的共同终点是:即便获得Ring-0执行权,CFG与CET等硬件级缓解已将间接控制流劫持压缩到几乎不可利用的狭缝。
结语
可以发现,唯一能够在HVCI下生存的进程隐藏方案,竟是一个使用官方文档化API实现的、看似乏味的暂态修复回调。
这本身就是对现代Windows安全架构的深刻注脚:EPT已将代码篡改从“难”变成了“不可能”;VTL1的隔离使防御逻辑彻底脱离攻击者的干扰域;残余的操作空间仅限于数据面,且每一步都必须抢在防御机制观察之前的极窄窗口内完成。
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:Ghost Wolf Lab Noair Noair《在HVCI与PatchGuard夹缝中操纵进程链表的微秒对决》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。










评论