【机翻】关于 ETW 内部结构:架构、钩子、篡改和检测(About ETW Internals: Architecture, Hooking, Tampering, and Detection )

目录

原文:About ETW Internals: Architecture, Hooking, Tampering, and Detection
作者:kernullist | 发布日期:2026年6月2日 | 阅读时间:175分钟
许可证:CC BY 4.0

Event Tracing for Windows 是现代 Windows 安全工作中大部分内容背后的遥测基础架构。EDR、反作弊系统、取证工具、WPR、Sysmon 相关管道以及许多 Microsoft 组件都依赖于它。攻击者也深知这一点,因此 ETW 既是信号源,也是攻击目标。本文从内部深入剖析 ETW:Provider 如何到达 Session,Buffer 和 Enable Slot 存在于何处,哪些部分是公共 API,哪些部分是私有内核状态,以及篡改实际上如何改变防御者所见的内容。参考目标是 Windows 11 25H2(ntoskrnl 10.0.26200.x),并标注 24H2 的差异。26H1(10.0.28000.x)在 Microsoft 2026 年 5 月发布表中作为限定新设备的版本公开,但本文中关于私有结构体的声明仍需要针对具体目标的符号和实时验证。

文章结构遵循 PCIe DMA 演练的模式:从威胁模型开始,深入到底层机制,再回到检测架构。重点不在于记住每个字段,而在于当某一层开始说谎时,知道应该由哪一层承载真相。

1. 威胁模型

本文档中的 ETW 攻击是指任何试图使有意义的 Windows 操作从防御者控制的一个或多个遥测视图中消失的尝试。该操作可能是进程创建、镜像加载、线程上下文操作、可执行内存分配、LSASS 内存读取、驱动加载、RPC 活动、AMSI 扫描或 Provider 自身的遥测。观察者可能是实时 ETW Consumer、AutoLogger ETL 文件、Sysmon、EDR 服务、内核回调账本、WinDbg 实时会话或离线内存转储。如果操作仍然发生,但防御者管道中每个有意义的 Consumer 要么未收到事件,要么收到被过滤的事件,要么收到上下文不再可信的事件,则攻击者即告成功。

每个执行有趣操作的 Windows 进程或驱动最终都会跨越 ETW 边界。进程创建通过 Microsoft-Windows-Kernel-Process,远程线程注入通过 Microsoft-Windows-Threat-Intelligence(ETWTI),LSASS 句柄获取通过 Microsoft-Windows-Kernel-Audit-API-Calls,PowerShell 脚本块编译通过 Microsoft-Windows-PowerShell 事件 4104,调度器在每次切换时向 Circular Kernel Context Logger 发出上下文切换事件。这就是为什么每个商业 EDR 在结构上都是一个 ETW Consumer 加上内核回调账本加上一些有针对性的内存扫描。这也是为什么每个内核模式作弊工具或后渗透工具包最终都要回答同一个问题:如何让这个加载器的工作不出现在 Consumer 端?

四个观察结论塑造了本文的其余部分:

  1. ETW 是路由器,不是日志文件。 高价值状态存在于 Provider 注册、Enable Slot、Session Buffer、Consumer 对象、过滤器、AutoLogger 注册表状态和每 Silo 驱动状态中。.etl 只是该路由架构的一种可能输出。

  2. 快速路径小到足以通过字节写入来破坏。 每个 ETW_REG_ENTRY 携带一个 EnableMask,每个 ETW_GUID_ENTRY 携带 ProviderEnableInfo.IsEnabled 加上最多八个 EnableInfo[] 槽位。清除一个字节或一个 ULONG 可以在不停止单个 Session 的情况下使 Provider 静默。

  3. 内核发起的 ETW 与用户态 ETW 在本质上不同。 修补 ntdll!EtwEventWrite 可以使单个进程内的 PowerShell 失明。它不能使 Microsoft-Windows-Kernel-Process 或 ETWTI 失明,因为这些事件源自执行工作的内核路径内部。

  4. Consumer 路径是安全边界的一部分。 如果 WMI_LOGGER_CONTEXT.AcceptNewEvents、Buffer 队列、实时 Consumer、过滤器或丢失计数器被破坏,Provider 可以处于启用状态但仍然无用。没有交付完整性的 Provider 完整性是不够的。

强信号贯穿本文:

信号 含义
ETWTI Provider 句柄为 NULL,或 ProviderEnableInfo.IsEnabled 意外下降 针对安全 Provider 路径的内核 DKOM
Provider 启用状态完好,但预期的金丝雀事件未到达 Consumer Session / Consumer / 交付路径被篡改
Microsoft-Windows-Kernel-EventTracing 停止报告 Session 变更,而其他活动继续 元 Provider 在篡改前被禁用
ETWTI 内存事件存在,但用户态系统调用遥测报告不同的参数 参数欺骗或系统调用后用户态欺骗
实时丢失计数器在狭窄的可疑窗口内攀升 Buffer 耗尽、Consumer 缓慢或故意洪泛
隐藏的 TraceLogging Provider 出现在实时注册中但不在 Manifest 注册表中 运行时 Provider、随机化 Provider 或未公开的 Windows 遥测面

防御者的决策过程与整个 Windows 隐藏文章中使用的跨视图不等式相同:

EtwTamperCandidate =
    ExpectedActivityObservedByStrongSource
    AND MissingOrContradictoryOnEtwPath
    AND NoLifecycleExplanation

具体来说:

KernelCallbackLedger shows remote VM write
AND ETWTI remote-write event is absent
AND ETWTI provider/session/canary health changed in the same window

这就是本文的工作模型。命名一个事件很容易。证明是哪一层说了谎才是真正的工作。

构建纪律很重要,因为 ETW 的公共 API 表面是稳定的,而私有内核布局会漂移。截至 2026-06-03 KST,Microsoft 的 Windows 11 发布表将 25H2 列为版本 26200,24H2 列为 26100,两者均通过 2026 年 5 月安全更新(26200.8457 / 26100.8457)和 2026 年 5 月 26 日预览更新(26200.8524 / 26100.8524)提供服务。Microsoft 还将 26H1 列为版本 28000(2026 年 5 月 26 日表中为 28000.2179),而 Windows SDK 页面列出了最新的 Windows SDK 为 10.0.28000。Microsoft 的发布页面明确将 26H1 限定为 2026 年初上市的新设备,并表示不在现有设备上提供从 24H2 或 25H2 的就地升级。本文使用的实时验证主机为 25H2 版本 26200.8524,本地安装了 SDK 10.0.26100.0,因此 25H2 仍然是实际的大众客户端基线;26H1 是公开的,但私有 ETW 布局声明仍需要针对特定目标的符号才能成为工程事实。

在操作上,这产生了三条规则:

  1. 使用 25H2 公共符号作为当前的大众客户端基线。 Vergilius 的 25H2 视图显示 _WMI_LOGGER_CONTEXT0x650_ETW_SILODRIVERSTATE0x1238_ETW_GUID_ENTRY0x1a8_ETW_REG_ENTRY0x70,与本文使用的字段在 24H2 公共布局中匹配。

  2. 使用 26100/28000 SDK 头文件获取 API 常量,而非内核偏移量。 evntrace.hevntprov.hevntcons.h 定义了过滤器常量、TraceQueryInformation 类、系统 Provider GUID、上下文寄存器跟踪和压缩标志。它们不定义私有内核布局。如果你的构建机器只有 26100 SDK,不要据此推断目标操作系统缺少 28000 时代的控制器类或 Provider GUID;应查询目标并针对实际发布的 SDK 进行构建。

  3. 发布前解析。 生产驱动、WinDbg 脚本、内存扫描器和 SOC 解析器应针对目标镜像解析偏移量和元数据:使用 PDB/DIA 解析私有结构,使用 TraceMaxLoggersQuery 获取 Session 容量,使用 TraceStreamCount 获取流拓扑,使用 logman query providers / wevtutil gp / Manifest 或 TraceLogging 元数据差异比对获取事件元数据,以及使用实时内核检查获取未公开的加固措施(如 KDP 保护的内存范围)。

2. ETW 拓扑

四个实体角色描述了 ETW 的所有功能。Provider 生成事件,由 128 位 GUID 标识。Session(也称为 Logger)拥有 Buffer 池和刷新线程,由一个小整数槽位(0..MaxLoggers-1)标识。Consumer 从 Session 读取,通过附加到其实时队列或读取 Session 刷新到磁盘的 .etl 文件。Controller 通过调用 EnableTraceEx2 启动和停止 Session 并将 Provider 绑定到它们。第四个角色对大多数读者来说是不可见的,因为 Controller 通常是 logmanwpr、EDR 服务或 xperf。它是唯一写入 ETW_GUID_ENTRY.EnableInfo[] 的实体,该字段用于开启事件。

Provider 和 Session 通过启用记录连接,而不是通过永久的 Provider 到 Consumer 对象。现代 Provider 在其 ETW_GUID_ENTRY 中最多有八个 TRACE_ENABLE_INFO 槽位。每个槽位,如果 IsEnabled 非零,则命名一个 Logger Session 以及该 Session 施加的关键字/级别/过滤器状态。在事件写入时,Provider 遍历八个槽位,按槽位过滤,并将一个副本写入每个匹配 Session 的流 Buffer。重要的属性不是"零副本";而是有界扇出。一个 Provider 可以馈送给多个 Session,但热路径永远不需要遍历无界的订阅者列表。

┌────────────────────────────────── USER MODE ────────────────────────────────┐
│   Provider              Controller              Consumer                    │
│   EventWrite            StartTrace              OpenTrace / ProcessTrace    │
│       │                 EnableTraceEx2                  │                   │
│       │ NtTraceEvent           │ NtTraceControl         │ NtOpenFile        │
└───────┼──────────────────────┼─────────────────────────┼───────────────────┘
        │                      │                         │
─ ─ ─ ─ ─│─ ─ ─ ─ ─ ─ ─ ─ ─ ─│─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─│─ ─ ─ ─ ─ ─ ─ ─ ─ ─
        ▼                      ▼                         │
┌────────────────────────────── KERNEL ───────────────────────────────────────┐
│  ntoskrnl ETW subsystem                                                     │
│    ETW_SILODRIVERSTATE     (PspHostSiloGlobals->EtwSiloState)                │
│      ├─ EtwpLoggerContext[MaxLoggers]  -> WMI_LOGGER_CONTEXT                 │
│      ├─ EtwpGuidHashTable[64]          -> ETW_GUID_ENTRY                     │
│      ├─ EtwpSecurityLoggers[8]                                              │
│      └─ EtwpCounters                                                        │
│                                                                             │
│    Per-CPU buffer pool  (WMI_BUFFER_HEADER + payloads)                      │
│    Per-session Logger Thread (flushes to .etl or to real-time consumer)     │
│                                                                             │
│  Kernel-mode providers                                                      │
│    EtwRegister -> ETW_REG_ENTRY linked into ETW_GUID_ENTRY.RegListHead       │
│    EtwWrite    -> reserves space in per-CPU buffer and emits event           │
└─────────────────────────────────────────────────────────────────────────────┘

此图中有两个表面对本文其余部分至关重要:

  • PspHostSiloGlobals->EtwSiloState 是入口点。所有其他 ETW 内核对象都通过从那里遍历来访问。在容器感知内核上,它是多个 EtwSiloState 实例之一;Host Silo 是默认的。
  • EtwpGuidHashTable[64] 按 GUID 索引系统中的每个 Provider。每个桶是一个 ETW_HASH_BUCKET,其 ListHead[0..2] 列表保存三种 GUID 类型(跟踪、通知、组)。发现"当前注册的每个 ETW Provider,包括没有 Manifest 的 TraceLogging Provider"简化为在 64 个桶中遍历这三个列表。

内核 ETW 初始化本身分两个阶段运行。EtwpInitialize(0) 注册一小部分核心 Provider(约 15 个,包括 ETWTI)并解析 Global Logger 配置;EtwpInitialize(1) 启动 CKCL,从注册表运行 AutoLogger,并完成内核 Provider 的注册。到第一个非启动驱动加载时,整个 ETW 拓扑已经就绪。这也是为什么如果 Secure Boot / ELAM 未被强制执行,早期启动 Rootkit 可以在阶段 0 和阶段 1 之间进行拦截。

3. 内核数据结构

以下结构是编写检测模块或可信利用原语所需的最小集合。字段偏移量在不同 Windows 版本之间会漂移,有时在单个功能更新内就会变化,因此生产代码必须通过 Vergilius/PDB 在运行时解析它们,或通过锚定到稳定签名来解析。注释基于 Windows 11 25H2 x64;24H2 公共布局与本文引用的字段匹配。

3.1 ETW_SILODRIVERSTATE

struct _ETW_SILODRIVERSTATE {
    PEPROCESS                      Silo;                            // +0x000
    PESERVER_SILO_GLOBALS          SiloGlobals;                     // +0x008
    ULONG                          MaxLoggers;                      // +0x010  // typically 0x50 (80)
    ETW_GUID_ENTRY                 EtwpSecurityProviderGuidEntry;   // +0x018
    PEX_RUNDOWN_REF_CACHE_AWARE*   EtwpLoggerRundown;               // +0x1C0
    PWMI_LOGGER_CONTEXT*           EtwpLoggerContext;               // +0x1C8  // array of size MaxLoggers
    ETW_HASH_BUCKET                EtwpGuidHashTable[64];           // +0x1D0
    USHORT                         EtwpSecurityLoggers[8];          // +0xFD0
    UCHAR                          EtwpSecurityProviderEnableMask;  // +0xFE0
    LONG                           EtwpShutdownInProgress;          // +0xFE4
    ULONG                          EtwpSecurityProviderPID;         // +0xFE8
    ETW_PRIV_HANDLE_DEMUX_TABLE    PrivHandleDemuxTable;            // +0xFF0
    PWCHAR                         RTBacklogFileRoot;               // +0x1010
    ETW_COUNTERS                   EtwpCounters;                    // +0x1018
    LARGE_INTEGER                  LogfileBytesWritten;             // +0x1028
    PETW_SILO_TRACING_BLOCK        ProcessorBlocks;                 // +0x1030
    ETW_SYSTEM_LOGGER_SETTINGS     SystemLoggerSettings;            // +0x1088
    KMUTANT                        EtwpStartTraceMutex;             // +0x1200
};

EtwpLoggerContext 是一个指针数组,而非内嵌数组。未使用的槽位不是 NULL。它们通常被观察为 (PWMI_LOGGER_CONTEXT)0x1,选择此哨兵值是为了使将槽位清零的部分 DKOM 在转储中在视觉上可区分。验证谓词:p != 0 && p != 1MaxLoggers 是数组长度;在 Windows 10 1709 之前,非私有 Logger 上限固定为 64,而现代 Windows 通过 TraceQueryInformation(..., TraceMaxLoggersQuery, ...) 暴露当前上限,并可通过 HKLM\SYSTEM\CurrentControlSet\Control\WMI\EtwMaxLoggers 提高它。

3.2 WMI_LOGGER_CONTEXT

每个 Session 的控制结构。在 25H2 和 24H2 上约为 0x650 字节。以下字段是实现 Session 或攻击 Session 所需的字段。

struct _WMI_LOGGER_CONTEXT {
    ULONG          LoggerId;             // 0..MaxLoggers-1
    ULONG          BufferSize;           // per-buffer size; default 64 KiB
    ULONG          MaximumEventSize;
    ULONG          LoggerMode;           // EVENT_TRACE_*_MODE flags
    LONG           AcceptNewEvents;      // 0 -> events refused
    ULONGLONG      GetCpuClock;          // old branches: function ptr;
                                         // modern branches: clock-source index
    PETHREAD       LoggerThread;         // flusher
    LONG           LoggerStatus;
    ULONG          FailureReason;
    ETW_BUFFER_QUEUE BufferQueue;        // file-mode flush queue
    ETW_BUFFER_QUEUE OverflowQueue;
    EX_FAST_REF    CurrentBuffer;        // active stream buffer fast ref
    LIST_ENTRY     GlobalList;
    UNICODE_STRING LoggerName;
    UNICODE_STRING LogFileName;
    ULONG          ClockType;
    ULONG          FlushTimer;
    ULONG          MinimumBuffers;
    volatile LONG  NumberOfBuffers;
    ULONG          MaximumBuffers;
    ULONG          EventsLost;           // monotonic loss counter
    LIST_ENTRY     Consumers;            // ETW_REALTIME_CONSUMER list
    ULONG          Flags;                // includes RealTime, SecurityTrace,
                                         // StackTracing, BootLogger, etc.
    ETW_STACK_TRACE_BLOCK StackTraceBlock;
    WMI_BUFFER_HEADER** ScratchArray;
    ETW_SILODRIVERSTATE* SiloState;
    LONG           CompressionOn;
};

这是有意精简的字段列表,但上述名称与 25H2 公共类型信息对齐。本文早期草稿经常显示 BufferTable[] 成员或独立的 SecurityTrace 字段;这对现代 Windows 来说是误导性的。流状态通过当前/批处理 Buffer 机制加上每 Silo 处理器跟踪块来访问,而 SecurityTraceFlags 中的一个位(公共类型视图中的 WMI_LOGGER_CONTEXT.Flags.SecurityTrace),而非独立的 USHORT

GetCpuClock 是对攻击性研究而言最重要的字段。在旧版本分支中,它持有一个函数指针;重写它(以 System Trace Logger 或 CKCL 为目标)可以在每次事件写入时获得控制权,InfinityHook 将其变成了系统调用 Hook。在现代版本分支中,该字段是时钟源选择器,而非任意函数指针。公共 24H2 逆向工程显示了系统时间、QPC、HAL 主机性能计数器和原始 TSC 路径的选择器值;确切的调度细节应针对目标 PDB 解析。重要的防御点比本文早期草稿暗示的更窄:将攻击者控制的内核指针写入 GetCpuClock 不再产生可调用的 Hook,但强制选择器进入到达可变下游 HAL 定时器回调的分支仍然可以产生 InfinityHook 风格的热路径 Hook(§10)。

3.3 ETW_GUID_ENTRY

struct _ETW_GUID_ENTRY {
    LIST_ENTRY        GuidList;              // bucket linkage in EtwpGuidHashTable
    LIST_ENTRY        SiloGuidList;          // all GUIDs in this silo
    volatile LONGLONG RefCount;
    GUID              Guid;
    LIST_ENTRY        RegListHead;           // ETW_REG_ENTRY list - every registration
    PVOID             SecurityDescriptor;
    union { ETW_LAST_ENABLE_INFO LastEnable; ULONGLONG MatchId; };
    TRACE_ENABLE_INFO ProviderEnableInfo;    // aggregate enable (the "is any session live?" bit)
    TRACE_ENABLE_INFO EnableInfo[8];         // per-session enable; up to 8 sessions
    PETW_FILTER_HEADER FilterData;
    PETW_SILODRIVERSTATE SiloState;
    PETW_GUID_ENTRY    HostEntry;            // points to host-silo entry from a container
    EX_PUSH_LOCK      Lock;
    PETHREAD          LockOwner;
};

上述两个启用字段是在 EnableTraceEx2 和 AutoLogger 启用期间变化的 Controller 可见状态。它们也是最小的可能攻击面:将 ProviderEnableInfo.IsEnabled 清零会禁用聚合 Provider 路径;将一个 EnableInfo[i].IsEnabled 槽位清零会禁用特定 Logger;将 Level 降级或将 MatchAnyKeyword 替换为 Provider 永远不会发出的关键字位可以使 Session 饿死。不要将 MatchAnyKeyword = 0 描述为阻止写入:在 ETW 关键字语义中,零 MatchAnyKeyword 通常意味着"匹配所有关键字",因此盲目清零会扩大而非缩小交付范围。

3.4 ETW_REG_ENTRY

struct _ETW_REG_ENTRY {
    LIST_ENTRY        RegList;                  // GuidEntry->RegListHead linkage
    LIST_ENTRY        GroupRegList;             // GroupEntry->RegListHead linkage
    PETW_GUID_ENTRY   GuidEntry;                // owning provider GUID entry
    PETW_GUID_ENTRY   GroupEntry;               // group GUID (NULL if not grouped)
    union {
        PETW_REPLY_QUEUE  ReplyQueue;
        PETW_QUEUE_ENTRY  ReplySlot[4];
        struct { PVOID Caller; ULONG SessionId; };
    };
    union { PEPROCESS Process; PVOID CallbackContext; };
    PVOID             Callback;                 // Enable callback function pointer
    USHORT            Index;
    union {
        USHORT Flags;
        struct {
            USHORT DbgKernelRegistration:1;
            USHORT DbgUserRegistration:1;
            USHORT DbgReplyRegistration:1;
            USHORT DbgClassicRegistration:1;
            USHORT DbgSessionSpaceRegistration:1;
            USHORT DbgModernRegistration:1;     // Manifest/TraceLogging
            USHORT DbgClosed:1;
            USHORT DbgInserted:1;
            USHORT DbgWow64:1;
            USHORT DbgUseDescriptorType:1;
            USHORT DbgDropProviderTraits:1;
        };
    };
    UCHAR             EnableMask;               // per-session enable bits (8 bits)
    UCHAR             GroupEnableMask;          // group enable
    UCHAR             HostEnableMask;
    UCHAR             HostGroupEnableMask;
    PETW_PROVIDER_TRAITS Traits;                // TraceLogging metadata
};

EtwRegister(内核)或 EventRegister(用户态)返回的 REGHANDLE 是指向其中一个结构的编码指针。Lazarus 的 FudModule 是经典的内核 ETW 消灭 Rootkit,它扫描 nt!.text 中的 nt!EtwRegister 调用点,回溯到存储结果句柄到全局变量的 EtwRegister 参数,跟随全局变量到其 .data 位置,并将全局变量清零。结果:EtwTi* 日志调用遇到 NULL 注册句柄,走死代码早期返回路径,并静默丢弃事件。同样的思路可以推广到任何注册句柄位于可写内核内存中的内核 Provider。

3.5 TRACE_ENABLE_INFO

struct _TRACE_ENABLE_INFO {
    ULONG     IsEnabled;        // 0 / 1
    UCHAR     Level;            // 1 (CRITICAL) ... 5 (VERBOSE)
    UCHAR     Reserved1;
    USHORT    LoggerId;         // session slot
    ULONG     EnableProperty;   // EVENT_ENABLE_PROPERTY_*
    ULONG     Reserved2;
    ULONGLONG MatchAnyKeyword;  // OR mask (event passes if any bit overlaps; 0 = pass all)
    ULONGLONG MatchAllKeyword;  // AND mask (event must have all these bits set; 0 = no constraint)
};

常见的现代 Provider 过滤测试在概念上是:

event_match = enable.IsEnabled
   && event.Level <= enable.Level
   && (enable.MatchAnyKeyword == 0 || (event.Keyword & enable.MatchAnyKeyword))
   && ((event.Keyword & enable.MatchAllKeyword) == enable.MatchAllKeyword);

级别过滤有一个重要的边界情况:事件级别 0 是 win:LogAlways。许多事件使用级别 1..5,如果启用级别被强制为 0 则会被排除,但 Level = 0 不是通用的"关闭"开关。对于篡改检测,将意外的级别降级视为可疑,但使用 IsEnabledEnableMask 和确切的关键字掩码作为权威状态。

3.6 ETW_REALTIME_CONSUMER

实时 Consumer 在 Consumer 进程的句柄表中创建一个类型为 EtwConsumer 的内核对象;后备结构位于 Session 的 Consumers 列表中:

struct _ETW_REALTIME_CONSUMER {
    LIST_ENTRY  Links;          // WMI_LOGGER_CONTEXT.Consumers
    PVOID       ProcessHandle;
    PEPROCESS   ProcessObject;  // consuming process
    PVOID       NextNotDelivered;
    PVOID       RealtimeConnectContext;
    PKEVENT     DisconnectEvent;
    PKEVENT     DataAvailableEvent;
    ULONG*      UserBufferCount;
    SINGLE_LIST_ENTRY* UserBufferListHead;
    ULONG       BuffersLost;
    ULONG       EmptyBuffersCount;
    USHORT      LoggerId;
    UCHAR       Flags;          // disconnected / notified / WOW64 etc.
    ULONG*      EventsLostCount;
    ULONG*      BuffersLostCount;
    ETW_SILODRIVERSTATE* SiloState;
};

遍历 EtwpLoggerContext[i]->Consumers 可以枚举 Session i 的每个活跃实时订阅者。这就是你从内核调试器中找出哪个用户态进程正在从 CKCL 或你的 ETWTI Session 读取的方式。同样的遍历也展示了攻击者在决定暂停什么之前如何枚举 EDR 的 Consumer 进程。

3.7 ETW_COUNTERS - 它不是什么

ETW_SILODRIVERSTATE.EtwpCounters(Win11 25H2/24H2 上偏移 +0x1018)经常被错误地描述为系统范围的事件丢失计数器块。它不是。25H2 公共类型是:

struct _ETW_COUNTERS {
    LONG GuidCount;
    LONG PoolUsage[2];
    LONG SessionCount;
};

将其用于粗粒度拓扑健全性检查,而非吞吐量健康状态。事件丢失和交付健康状态存在于 Session 上,通过公共 EVENT_TRACE_PROPERTIES 查询输出(EventsLostLogBuffersLostRealTimeBuffersLostNumberOfBuffersFreeBuffers)和内部 WMI_LOGGER_CONTEXT 字段(EventsLostLogBuffersLostRealTimeBuffersLostBuffersWritten)暴露。实用的健康监视器使用 ControlTrace(..., EVENT_TRACE_CONTROL_QUERY, ...) 采样每个 Session,仅当实时调试器或驱动已具有可信偏移量时才回退到内核字段。

3.8 EVENT_TRACE_PROPERTIES 和 WNODE_HEADER

任何 Session 控制调用(StartTraceControlTraceTraceSetInformation)的用户态侧传递一个 EVENT_TRACE_PROPERTIES(或其 V2 变体),其第一个成员是遗留的 WNODE_HEADER。该头部是 ETW 所取代的原始 WMI Trace 基础设施的遗迹,但布局对于内核侧解析仍然是承载性的:

typedef struct _WNODE_HEADER {
    ULONG          BufferSize;            // total size of the properties block
    ULONG          ProviderId;            // legacy MOF source identifier
    union {
        ULONG64    HistoricalContext;     // TRACEHANDLE for control operations
        struct {
            ULONG  Version;
            ULONG  Linkage;
        };
    };
    union {
        HANDLE        KernelHandle;
        LARGE_INTEGER TimeStamp;
    };
    GUID           Guid;                  // session GUID (or system trace control GUID)
    ULONG          ClientContext;         // clock type (1=QPC, 2=SystemTime, 3=CPU cycle)
    ULONG          Flags;                 // WNODE_FLAG_* (e.g. WNODE_FLAG_TRACED_GUID,
                                          // WNODE_FLAG_VERSIONED_PROPERTIES for V2)
} WNODE_HEADER, *PWNODE_HEADER;

两个实用要点:(1) BufferSize 在每次调用时都会被检查,必须包括尾部的 Logger 名称/日志文件名 UTF-16 字符串,这是手写代码中 StartTrace 返回 ERROR_BAD_LENGTH 最常见的原因;(2) WNODE_FLAG_VERSIONED_PROPERTIES 切换 V2 模式,是 EVENT_TRACE_PROPERTIES_V2.FilterDescCount / FilterDesc / V2Options 的先决条件,包括 §14.6 中描述的系统范围私有 Logger PID 或可执行文件名过滤器。

3.9 容器感知 - Silo 隔离

Windows 10 引入的服务器 Silo(支持 Windows 主机上 docker run 的操作系统级容器原语)使 ETW 具备了 Silo 感知能力。结果是不存在单一的全局 ETW 状态。有 N 个 ETW_SILODRIVERSTATE 实例,每个 Silo 一个,Host Silo(PspHostSiloGlobals)是默认的。容器中的进程有自己的 EtwpLoggerContext、自己的 EtwpGuidHashTable 和自己的 Provider 注册。容器内部事件不会跨越 Silo 边界,除非 Host 通过容器的 Silo 状态显式订阅。

相关的桥接字段是 ETW_GUID_ENTRY.HostEntry。当同一个 Provider 在 Host Silo 和容器 Silo 中都注册时,容器的条目通过 HostEntry 指回 Host 的条目,允许内核分层路由 Host 已订阅的事件,即使它们源自容器内部。取证的后果是,对容器化工作负载的事件分析必须遍历 Silo 的 EtwSiloState 而非 PspHostSiloGlobals。到达内核模式的容器逃逸攻击者可以读取或篡改 Host 的 ETW 状态,这是容器-Host 分离需要 HVCI 才能可信的最强原因之一。

同样的 EtwSiloState 管道还将 EVENT_HEADER_EXT_TYPE_CONTAINER_ID 扩展项传播到从容器内部发出的事件上,因此 Host 侧的 Consumer 可以将每个事件归因于其源容器。

3.10 现代高端 Session 字段

上面精简的 WMI_LOGGER_CONTEXT 布局足以推理 Provider 启用和基本交付,但 25H2 的公共布局暴露了第二个字段集群,对高保真 EDR、反作弊和性能取证工作很重要:

ETW_STACK_TRACE_BLOCK StackTraceBlock;          // +0x340
RTL_BITMAP            StackHookIdMap;           // +0x410
ETW_STACK_CACHE*      StackCache;               // +0x420
ETW_PMC_SUPPORT*      PmcData;                  // +0x428
ETW_LBR_SUPPORT*      LbrData;                  // +0x430
ETW_IPT_SUPPORT*      IptData;                  // +0x438
ETW_APC_POOL          ContextRegisterLoggingApcPool;
volatile ULONG        ContextRegisterTypes;     // control/integer register classes
volatile ULONG        ContextRegisterHookCount;
USHORT                ContextRegisterHookIdMap[8];
LIST_ENTRY            BinaryTrackingList;       // provider-to-image correlation

这些字段解释了为什么"Session"不仅仅是 Buffer 池。Session 可以要求内核附加栈、将栈去重为缓存键、采样 PMC、收集 Last Branch Record 快照、跟踪 Provider 二进制文件以及为选定的系统 Provider 事件收集寄存器状态。它们还解释了用户态 Consumer 中几个令人困惑的扩展数据项:PMC_COUNTERSPEBS_INDEXSTACK_KEY32/64PROV_TRAITSEVENT_SCHEMA_TL 不是解析器的好奇产物;它们是这些 Session 功能被启用后的序列化副作用。

在操作上,这些字段应被视为能力状态,而非稳定的控制 ABI。公共 Controller 表面是 TraceSetInformation / TraceQueryInformation 加上 ENABLE_TRACE_PARAMETERS.EnableProperty;私有内核字段是验证目标。想要审计 EDR Session 的驱动可以将 PmcDataLbrDataStackCacheContextRegisterTypesBinaryTrackingList 与 Controller 的预期配置进行比较,但必须先在目标版本上解析偏移量。

4. Provider

4.1 四种 Provider 类型

类型 API Schema 位置 最大并发 Session 数 备注
Classic (MOF) RegisterTraceGuids / TraceEvent MOF 类 1 Vista 之前的 Provider 模型;不要将其与特殊的 NT Kernel Logger Session 混淆
WPP DoTraceMessage TMF / PDB 1 面向驱动调试;负载格式不在二进制文件中
基于 Manifest EventRegister / EventWrite WEVT_TEMPLATE 资源中的 XML Manifest 8 Vista 以来的默认类型
TraceLogging TraceLoggingWrite 事件负载中自描述 8 Schema 随事件携带;不需要 Manifest

"最大并发 Session 数"列是承载性区分。Classic 或 WPP Provider 一次只能被一个跟踪 Session 启用;Manifest 和 TraceLogging Provider 可以被最多八个 Session 启用。该 Provider 并发规则与 Session 名称所有权是分开的。对已运行的 Session 名称进行第二次 StartTrace 通常会因 ERROR_ALREADY_EXISTS 失败,而为另一个 Session 第二次尝试启用已拥有的 Classic/WPP Provider 可能会因 ERROR_INVALID_PARAMETERERROR_ACCESS_DENIED 失败,具体取决于路径。NT Kernel Logger 是一个特殊的系统跟踪 Session,而不仅仅是 Classic Provider,但它有同样的实际单一所有者问题:两个都想使用历史 KERNEL_LOGGER_NAME 原始内核馈送的产品会互相冲突,除非它们通过 System Trace Provider 多路复用(Windows 8+)或通过 Windows 10 SDK 20348 System Provider 按类别拆分(§14.8)进行协作。

4.2 内核注册流程

EtwRegister(ProviderId, Callback, CallbackContext, OutRegHandle)
  └─ EtwpRegisterKMProvider()
       ├─ EtwpGetOrCreateGuidEntry(ProviderId)        // walks EtwpGuidHashTable
       ├─ allocate ETW_REG_ENTRY from NonPagedPool
       ├─ RegEntry->GuidEntry        = GuidEntry
       ├─ RegEntry->Callback         = Callback
       ├─ RegEntry->CallbackContext  = CallbackContext
       ├─ RegEntry->DbgKernelRegistration = 1
       ├─ insert RegEntry into GuidEntry->RegListHead
       └─ *OutRegHandle = RegEntry

如果在注册时已有 Session 为此 GUID 启用,EtwRegister 会在返回前调用 Provider 的 EnableCallback。因此 Provider 回调必须是可重入安全的。任何"首次启用,分配资源"的模式如果不加锁就会与并发禁用产生竞态。这是第三方 Provider 中多个历史 CVE 的根源;标准模式是使用 KGUARDED_MUTEX 保护回调,并使用布尔值来门控分配/释放对。

4.3 用户态注册

用户态遵循 ntdll!EtwEventRegister -> NtTraceEvent -> 内核 EtwpRegisterUMProvider,创建一个 ETW_REG_ENTRY,其中 DbgUserRegistration=1Process 设置为调用者的 EPROCESS。用户态 REGHANDLE 是内核 ETW_REG_ENTRY 指针的编码表示(基于 XOR/偏移的编码而非原始指针);该编码在用户态 SEH 和进程挂起/恢复过程中保持不变。

这里有三个状态片段,混淆它们会导致错误的检测器:

  1. 进程本地句柄和包装器状态。 EventRegister 给进程一个 REGHANDLE;生成的 Manifest 代码、TraceLogging、.NET EventSource 和许多内部包装器缓存该句柄并围绕它缓存"启用"决策。修补此层只会使该进程和使用修补包装器的代码路径失明。

  2. 内核注册条目。 内核拥有 ETW_REG_ENTRY,将其链接到 Provider 的 ETW_GUID_ENTRY 下,并将用户态注册与拥有进程关联。这是 BYOVD 攻击者在不想触碰 ntdll 时编辑以使 Provider 静默的状态。

  3. 可选的注册元数据。 EventSetInformation(EventProviderSetTraits) 将二进制特征 Blob 附加到注册。TraceLogging 在 TraceLoggingRegister 期间自动设置特征;Manifest Provider 可以手动选择加入。特征 Blob 可以包括 UTF-8 Provider 名称和一个或多个 Provider 组 GUID。它在注册的生命周期内存储在内核内存中,并可以作为 EVENT_HEADER_EXT_TYPE_PROV_TRAITS 序列化到事件中。

EventSetInformation 还暴露了对解析器重要的两个细节。EventProviderBinaryTrackInfo 让 ETW 添加包含 Provider 回调的模块的完整路径,这有助于解码器在 Provider 未全局安装时找到 Manifest 资源。EventProviderUseDescriptorTypeEventProviderSetTraits 告诉 ETW 遵循 EVENT_DATA_DESCRIPTOR 内部的 Type 字段;旧 Provider 如果未初始化旧的 Reserved 字段,则不得意外选择进入此路径。

安全含义微妙但有用。安装在 HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\WINEVT\Publishers 下的 Manifest 告诉 Consumer 如何解码事件;它不证明 Provider 处于活跃状态。活跃的 ETW_REG_ENTRY 证明进程注册了 Provider;它不证明 Manifest 已安装。因此,健壮的运行时 Provider 清单需要连接三个视图:注册表 Manifest 清单、实时 EnumerateTraceGuidsEx/内核哈希表注册以及从二进制文件静态提取 TraceLogging/Provider 特征。

4.4 GUID 哈希表和查找

enum ETW_GUID_TYPE {
    EtwpTraceGuidType        = 0,   // ordinary providers
    EtwpNotificationGuidType = 1,   // notification providers (bidirectional)
    EtwpGroupGuidType        = 2,   // group GUIDs (§13.3)
};

struct _ETW_HASH_BUCKET {
    LIST_ENTRY ListHead[3];   // one list per ETW_GUID_TYPE
};

哈希值从 GUID 字节通过 XOR/加法混合计算并取模 64;确切的混合方式是私有的且偶尔会变化,因此逆向工程 nt!EtwpGetGuidEntry 是计算与内核相同桶的唯一可靠方法。对于枚举,不需要重新计算哈希。在 64 个桶的三个列表中遍历既快速又完整。

// WinDbg DX - enumerate every registered provider GUID across all three categories
function EnumerateAllProviderGuids() {
    const silo = host.createTypedObject(
        host.getModuleSymbolAddress("nt", "PspHostSiloGlobals"),
        "nt", "_ESERVERSILO_GLOBALS");
    const table = silo.EtwSiloState.EtwpGuidHashTable;
    const out = [];
    for (const bucket of table) {
        for (let t = 0; t < 3; t++) {
            const entries = host.namespace.Debugger.Utility.Collections
                .FromListEntry(bucket.ListHead[t], "nt!_ETW_GUID_ENTRY", "GuidList");
            for (const e of entries) {
                out.push({ Guid: e.Guid.toString(),
                           Type: ["Trace","Notification","Group"][t],
                           Regs: e.RegListHead.NumEntries });
            }
        }
    }
    return out;
}

这是发现没有 HKLM\...\WINEVT\Publishers 下 Manifest 条目的 TraceLogging Provider 的唯一方法,也是 wevtutil ep 因此不会列出的 Provider,包括内核驱动或 EDR 在运行时以随机 GUID 注册的临时 Provider。

5. Session

5.1 槽位布局

槽位 保留用途
0 NT Kernel Logger(SystemTraceControlGuid,Classic)。通常在新启动的主机上位于槽位 0。
1 Circular Kernel Context Logger(CKCL)。在阶段 1 初始化期间启动;通常观察到在槽位 1,但确切槽位在架构上不保证,可能根据启动时控制器顺序而变化。
2 .. MaxLoggers-1 通用用户/EDR Session。在 Windows 10 1709 之前,非私有 Session 的历史上限为 64 个;现代主机应使用 TraceMaxLoggersQuery 查询,而非假设固定的槽位数。

CKCL 通常处于开启状态,这也是 InfinityHook 首选目标具有吸引力的原因。攻击代码通过其众所周知的 GUID {54dea73a-ed1f-42a4-af71-3e63d056f174} 和匹配的 LoggerName 来识别 CKCL,而非硬编码的槽位索引。NT Kernel Logger 由 xperftracerpt 或 EDR 按需启动。Windows 8 及更高版本支持使用 EVENT_TRACE_SYSTEM_LOGGER_MODE 最多八个系统 Logger Session,Windows 10 SDK 20348+ 通过 System Provider GUID 暴露许多 System Trace Provider 类别(§14.8)。这改善了共存性,但并不意味着任意控制器都可以安全地修改一个共享的槽位 0 Logger。

5.2 特殊 Session 类型

  • AutoLogger。HKLM\SYSTEM\CurrentControlSet\Control\WMI\Autologger\<name> 下配置;在用户态服务启动之前由内核在启动期间启动。由 ELAM 驱动、Defender 和 AutoLogger-Diagtrack-Listener(遥测)使用。
  • GlobalLogger。HKLM\SYSTEM\CurrentControlSet\Control\WMI\GlobalLogger 下配置;在 EtwpInitialize(0) 期间启动,因此在任何服务运行之前就能捕获事件——这对启动驱动排查和取证至关重要,因为启动链的其余部分可能已被篡改。
  • Private Logger。 在 Provider 进程中运行,而非作为普通全局 Session。Buffer 内存来自进程,Private Session 不能用于实时交付,也不以与普通 Session 相同的方式消耗全局 Logger 槽位。实际限制容易混淆:当前文档说 Private Session 每进程最多八个,而 EVENT_TRACE_PRIVATE_IN_PROC 子模式在文档路径上限制为每进程三个进程内 Private Session。
    • EVENT_TRACE_PRIVATE_LOGGER_MODE - 创建一个用户态私有事件跟踪 Session,其 Buffer 由进程支持。
    • EVENT_TRACE_PRIVATE_IN_PROC - 与 Private Logger 模式一起使用,要求只有注册了 Provider GUID 的进程才能启动进程内 Private Logger。不要将其与 EventActivityIdControldotnet-trace 混淆。EventActivityIdControl 仅管理当前线程的活动 ID;dotnet-trace 主要使用 EventPipe,也可以启用 CLR ETW Provider,但它在 ETW 意义上不是"由 Private Logger 模式实现"的。
  • System-Wide Private LoggerEVENT_TRACE_PRIVATE_LOGGER_MODE | EVENT_TRACE_PRIVATE_IN_PROC,V2 属性)。一种特权 Private Logger 变体,使用 V2 Session 过滤器(EVENT_FILTER_TYPE_PID 或可执行文件名范围过滤器)来定位其他进程。它适用于"跟踪此可疑 PID 六十秒"的收集,但由于 Private Session 不支持实时交付,产物是文件/缓冲诊断跟踪而非实时 Consumer 流。

5.3 Session 生命周期

StartTraceW() -> NtTraceControl(EtwpStartTrace)
   EtwpStartLogger()
     ├─ acquire EtwpStartTraceMutex
     ├─ allocate WMI_LOGGER_CONTEXT from NonPagedPool
     ├─ initialize stream buffers (MinimumBuffers, MaximumBuffers, current buffer refs)
     ├─ PsCreateSystemThread for the Logger Thread
     └─ EtwpLoggerContext[loggerId] = ctx

EnableTraceEx2() -> NtTraceControl(EtwpEnableProvider)
   EtwpEnableProvider()
     ├─ EtwpGetOrCreateGuidEntry()
     ├─ EnableInfo[loggerId] = { IsEnabled=1, Level, MatchAny/All, LoggerId }
     ├─ recompute ProviderEnableInfo as union of EnableInfo[0..7]
     └─ for each ETW_REG_ENTRY on GuidEntry->RegListHead:
            invoke RegEntry->Callback(EVENT_CONTROL_CODE_ENABLE_PROVIDER, ...)

关键不变量:Provider 的 EnableMask 是已启用槽位的位掩码。事件时间的第一个检查是 if (RegEntry->EnableMask == 0) return STATUS_SUCCESS;。这单个字节是快速路径谓词。每个触及内核 ETW 的攻击技术最终都针对此字节、相应的 EnableInfo[i].IsEnabledLevel/MatchAnyKeyword 过滤器。

生命周期有比 StartTraceStopTrace 更多的状态:

操作 公共 API 内部效果 失败/篡改信号
Start StartTrace 分配 WMI_LOGGER_CONTEXT、Buffer、Logger Worker 和 EtwpLoggerContext[] 中的槽位。 槽位被占用但 LoggerStatus 或 Worker 状态与 Controller 状态不匹配。
Enable EnableTraceEx2 写入 Provider EnableInfo[],重新计算聚合 Provider 状态,调用回调。 Controller 认为 Provider 已启用,但 Provider 槽位或回调状态不一致。
Update ControlTrace(EVENT_TRACE_CONTROL_UPDATE) / TraceSetInformation 修改文件路径、刷新定时器、过滤器、栈设置、上下文寄存器/LBR/PMC 选项(在支持的情况下)。 过滤器或捕获设置与策略账本不一致。
Flush FlushTrace / ControlTrace(EVENT_TRACE_CONTROL_FLUSH) 强制脏或部分填充的 Buffer 通过 Logger Worker。 Flush 返回成功但 BuffersWritten/Consumer 金丝雀未推进。
Disable EnableTraceEx2(..., EVENT_CONTROL_CODE_DISABLE_PROVIDER) 清除 Provider 槽位并调用禁用回调。 Provider 在禁用后仍发出事件,或回调拥有的状态未释放。
Stop ControlTrace(EVENT_TRACE_CONTROL_STOP) 刷新、取消链接 Logger Context、拆除 Buffer 和 Consumer 附件。 元 Provider 没有停止事件,但槽位消失或文件关闭。

此表是防御者的对账模型。产品应该知道它启动了哪些 Session、启用了哪些 Provider、打算使用哪些过滤器、请求了哪种文件/实时模式,以及上次看到金丝雀的时间。其他一切都是伪装成监控的猜测。

6. 无锁事件路径

ETW 的事件写入路径是触及每个有趣内核操作的性能最关键的代码。那里的设计决策既解释了为什么快速路径基本上是免费的,也解释了为什么慢路径绕过起来并不简单。

6.1 Buffer Header

struct _WMI_BUFFER_HEADER {
    ULONG    BufferSize;
    ULONG    SavedOffset;
    volatile ULONG  CurrentOffset;     // Interlocked-updated
    volatile LONG   ReferenceCount;
    LARGE_INTEGER   TimeStamp;
    LONGLONG        SequenceNumber;    // event ordering across CPUs
    union { ULONG Filled; ULONG NextBuffer; };
    ULONG    LoggerId;
    ETW_BUFFER_STATE  State;           // FREE / DIRTY / FLUSH
    ULONG    Offset;
    USHORT   BufferFlag;
    USHORT   BufferType;               // GENERIC / RUNDOWN / CTX_SWAP ...
};

Buffer 负载是 8 字节对齐的事件记录序列,每个记录以 EVENT_HEADER(现代)或 SYSTEM_TRACE_HEADER(Classic)为前缀。事件不会跨越 Buffer 边界。如果写入会溢出,写入者先轮换 Buffer。一个公共约束在生产中很重要:ETW 不支持包括事件头在内的单个事件大于 64 KB。将 EVENT_TRACE_PROPERTIES.BufferSize 增加到 64 KB 以上有助于吞吐量,但不会使超大事件有效。

6.2 Per-CPU Buffer

正常的高吞吐量 Session 是流缓冲的,通常每个逻辑处理器一个流,除非 EVENT_TRACE_NO_PER_PROCESSOR_BUFFERING 将 Session 折叠为更少的流。询问 Session 使用多少流的公共方式是 TraceQueryInformation(..., TraceStreamCount, ...);它通常等于处理器数量,但在架构上不保证。写入通过原子更新当前流的活跃 WMI_BUFFER_HEADER.CurrentOffset 来预留空间。每流设计使 ETW 能够在从 DPC 密集路径调用时存活,而不会让一个全局日志锁成为瓶颈。

6.3 事件写入 - 快速路径 vs. 慢速路径

NTSTATUS EtwpWriteEvent(PETW_REG_ENTRY reg, PEVENT_DESCRIPTOR desc,
                       EVENT_DATA_DESCRIPTOR* data, ...) {
    // FAST PATH - tiny predicate set
    if (!reg->EnableMask)
        return STATUS_SUCCESS;

    if (!reg->GuidEntry->ProviderEnableInfo.IsEnabled)
        return STATUS_SUCCESS;

    // SLOW PATH - for each enabled session, filter & reserve
    for (int i = 0; i < 8; i++) {
        TRACE_ENABLE_INFO* en = &reg->GuidEntry->EnableInfo[i];
        if (!en->IsEnabled) continue;
        if (desc->Level > en->Level) continue;
        if (en->MatchAnyKeyword && !(desc->Keyword & en->MatchAnyKeyword)) continue;
        if ((desc->Keyword & en->MatchAllKeyword) != en->MatchAllKeyword) continue;

        ULONG stream = EtwpGetCurrentStreamIndex();
        PWMI_LOGGER_CONTEXT ctx = EtwpLoggerContext[en->LoggerId];
        ULONG sz = ComputeEventSize(desc, data);
        PWMI_BUFFER_HEADER buf = EtwpGetCurrentBuffer(ctx, stream);
        ULONG off = InterlockedExchangeAdd(&buf->CurrentOffset, sz);
        if (off + sz > ctx->BufferSize) {
            EtwpBufferFull(ctx, stream);   // rotate, signal logger thread; may lose if pool exhausted
            continue;
        }

        WriteEventHeader(buf + off, desc, /* timestamp from ctx->GetCpuClock dispatch */);
        CopyEventData(buf + off, data);

        if (en->EnableProperty & EVENT_ENABLE_PROPERTY_STACK_TRACE)
            AttachStackTrace(buf + off);
    }
    return STATUS_SUCCESS;
}

由此产生两个操作特性:

  • 空闲 Provider 的开销约为十个周期。 快速路径未命中是一次加载和一个分支。这就是为什么数千个已注册的 Provider 不会拖慢系统——以及为什么 EnableMask 被强制为 0 的被篡改 Provider 与合法静默的 Provider 不可区分,除非你检查 EnableInfo[] 或与 Controller 声称的订阅进行关联。

  • 溢出的 Session 会丢失事件。 WMI_LOGGER_CONTEXT 上的 EventsLost 字段是单调丢失计数器;突然的增量表明 Buffer 大小配置错误或故意洪泛试图将攻击者活动推过刷新器。防御性收集器必须像 SOC 监控"日志源静默"警报一样监控此增量。

IRQL:内核 EtwWrite DDI 文档记录为可在任何 IRQL 调用,但 EVENT_DATA_DESCRIPTOR 描述的 Buffer 必须在该 IRQL 下有效。在 APC_LEVEL 以上,这意味着不可分页的、系统可寻址的内存。栈遍历等可选功能有更窄的实际约束,即使事件本身被交付,也可能被省略。

6.4 Logger 线程

每个 Session 有一个 Logger Worker,将满 Buffer 转换为持久或可消费的输出。事件写入路径不同步调用 Consumer,也不同步写入 ETL 文件。它预留空间、复制记录、在 Buffer 填满或请求刷新时标记为脏,并唤醒 Logger。这种分离是为什么 ETW 在热路径上可以便宜而在背压下仍可能丢失事件的原因。

Worker 因四个普通原因唤醒:

唤醒原因 发生什么
Buffer 满 Per-CPU Buffer 达到其提交限制并移至脏队列。
刷新定时器 EVENT_TRACE_PROPERTIES.FlushTimer 到期;部分填充的 Buffer 有资格刷新。
显式刷新 Controller 调用了 FlushTrace / ControlTrace(EVENT_TRACE_CONTROL_FLUSH) 或停止了 Session。
实时 Consumer 压力 Consumer 附加或实时交接路径需要 Buffer 交付。

文件模式和实时 Session 在此处分道扬镳。在文件模式下,脏 Buffer 被写入日志文件并回收。在实时模式下,Logger 使 Buffer 对附加的 Consumer 可见并跟踪交付进度;如果 Consumer 缓慢,内核侧回放路径可能成为瓶颈。混合 Session(EVENT_TRACE_REAL_TIME_MODE 加文件输出)承担两种成本,这对证据持久性有用,但如果 Consumer 回调缓慢则很危险。

丢失计数器告诉你哪一侧出了问题:

  • EventsLost 意味着事件根本无法写入 Session Buffer。这通常表示 Buffer 池耗尽或事件大于可用 Buffer。
  • LogBuffersLost 意味着脏 Buffer 无法足够快地写入文件路径,或文件路径失败。
  • RealTimeBuffersLost 意味着 Buffer 无法足够快地交付给实时 Consumer。
  • BuffersWrittenNumberOfBuffersFreeBuffers 告诉你 Session 是仅仅繁忙还是实际上饥饿。

对于检测工程,Logger 线程是一流的篡改面。清除 Provider 状态使生产者失明;卡住 Logger 使交付失明而保持 Provider 状态正确。一个启用的、每 N 秒写入一次的金丝雀 Provider,预期在 FlushTimer + 抖动 内到达 Consumer,是最简单的端到端测试。如果 Provider 启用状态完好而金丝雀消失,同时 RealTimeBuffersLostEventsLost 增长,则故障在 Session 交付,而非 Provider 注册。

6.5 栈遍历

栈捕获是事件写入路径中最昂贵的可选功能,也是具有最多版本敏感行为的功能。启用它是 Controller 侧的决定:

ENABLE_TRACE_PARAMETERS params = {0};
params.Version          = ENABLE_TRACE_PARAMETERS_VERSION_2;
params.EnableProperty   = EVENT_ENABLE_PROPERTY_STACK_TRACE;
// or, with per-event-ID precision:
params.FilterDescCount  = 1;
params.EnableFilterDesc = &eventIdFilter;   // EVENT_FILTER_TYPE_STACKWALK

内核捕获到 EVENT_HEADER_EXT_TYPE_STACK_TRACE32_TRACE64 扩展项中,最多 192 帧,在与事件负载相同的 Buffer 中。栈的两半,内核和用户,被捕获到单独的扩展项中并通过共享的 MatchId 字段关联,Consumer 重新组装它们。成本约为每个事件 1,000-5,000 个周期;在繁忙的主机上全局启用它是将 Session 推入 EventsLost 最可靠的方式。

三个操作约束塑造了栈遍历实际捕获的内容:

  1. IRQL 和可分页性仍然重要。 EtwWrite 本身文档记录为可在任何 IRQL 调用,但在 APC_LEVEL 以上传递的数据必须不可分页且在系统空间中。栈捕获比基本写入路径更受限;如果内核无法安全遍历请求的栈,事件仍然可以在没有扩展栈项的情况下交付。依赖栈进行归因的防御代码因此必须容忍栈的缺失。

  2. x64 上的内核栈捕获不是免费的生产默认值。 Microsoft 文档记录 DisablePagingExecutive = 1 作为改善内核栈遍历的方法,但也警告它应该是临时诊断,因为它增加内存压力。生产收集器应优先使用有针对性的栈过滤器,测量丢失,并在用户态归因足够时考虑 V2 ExcludeKernelStack 选项。

  3. 内核栈深度。 x64 内核栈默认为 24 KiB(六页);深度递归路径可能在捕获期间耗尽栈保护页,内核通过截断来处理。扩展项携带截断前容纳的任意多帧。

在此上下文中,§13.2 中最有用的过滤器是 EVENT_FILTER_TYPE_STACKWALK,它仅为枚举的事件 ID 集启用捕获。将栈遍历连接到 ETWTI 可执行分配/保护/映射、VM 读/写、APC 队列、设置线程上下文、挂起/恢复、模拟和驱动/设备拓扑的行为事件是典型的 EDR 配置。这些正是调用栈在取证上承载关键性的事件,ETW 的其余部分可以在没有栈遍历的情况下运行。句柄打开/复制分析应使用 Microsoft-Windows-Kernel-Audit-API-Calls 加上 ObRegisterCallbacks;不要假设当前 ETWTI 元数据携带旧的复制句柄 ID。

对于针对现代对手的可信栈遍历归因,你不能止步于"启用过滤器"。帧欺骗技术(§11.6)可以击败朴素遍历;CET 影子栈可以捕获其中许多,但仅在支持它的硬件上。端到端设计模式是:为安全关键事件 ID 捕获栈,在可用时验证影子栈一致性,并将任何看起来像欺骗的帧链(ntdll!RtlUserThreadStart 没有合理的内部帧,RSP 增量暗示展开超出栈底页)视为高置信度的遥测篡改信号。

7. 事件格式和 ETL 文件

7.1 现代 EVENT_HEADER

struct EVENT_HEADER {
    USHORT           Size;
    USHORT           HeaderType;        // always 0 for modern
    USHORT           Flags;             // EVENT_HEADER_FLAG_*
    USHORT           EventProperty;
    ULONG            ThreadId;
    ULONG            ProcessId;
    LARGE_INTEGER    TimeStamp;         // QPC or system time per GetCpuClock dispatch
    GUID             ProviderId;
    EVENT_DESCRIPTOR EventDescriptor;   // {Id, Version, Channel, Level, Opcode, Task, Keyword}
    union { struct { ULONG KernelTime; ULONG UserTime; }; ULONG64 ProcessorTime; };
    GUID             ActivityId;
};

对取证和 Provider 分类有用的头部标志:

  • EVENT_HEADER_FLAG_EXTENDED_INFO (0x0001) - 负载后跟 EVENT_HEADER_EXTENDED_DATA_ITEM 数组
  • EVENT_HEADER_FLAG_PRIVATE_SESSION (0x0002) - 由 Private Logger Session 发出
  • EVENT_HEADER_FLAG_STRING_ONLY (0x0004) - 负载是裸 UTF-16 字符串
  • EVENT_HEADER_FLAG_TRACE_MESSAGE (0x0008) - WPP TraceMessage(不是 TraceLogging - 见 §7.3)
  • EVENT_HEADER_FLAG_32_BIT_HEADER / _64_BIT_HEADER - 源进程位数

7.2 扩展数据项

struct EVENT_HEADER_EXTENDED_DATA_ITEM {
    USHORT    Reserved1;
    USHORT    ExtType;       // EVENT_HEADER_EXT_TYPE_*
    USHORT    Linkage : 1;
    USHORT    Reserved2 : 15;
    USHORT    DataSize;
    ULONGLONG DataPtr;
};
ExtType 用途
RELATED_ACTIVITYID (0x01) 父活动 GUID(见 §13)
SID (0x02) 写入者的用户 SID
TS_ID (0x03) 终端服务 Session ID
INSTANCE_INFO (0x04) 实例关联
STACK_TRACE32 / STACK_TRACE64 (0x05 / 0x06) 内核 + 用户调用栈,最多 192 帧
PEBS_INDEX (0x07) Intel PEBS 采样索引
PMC_COUNTERS (0x08) 硬件性能监控计数器
PSM_KEY (0x09) 进程状态管理密钥
EVENT_KEY (0x0A) 每事件不透明密钥
EVENT_SCHEMA_TL (0x0B) TraceLogging Schema(用于 §13.6 动态提取)
PROV_TRAITS (0x0C) Provider 特征 Blob
PROCESS_START_KEY (0x0D) 在 PID 重用中存活的稳定进程标识
CONTROL_GUID (0x0E) 控制 GUID
QPC_DELTA (0x0F) QPC 增量值(与 QpcDeltaTracking V2 选项一起使用)
CONTAINER_ID (0x10) Silo / 容器 ID
STACK_KEY32 (0x11) 32 位栈缓存键(TraceStackCachingInfo 去重开启时使用)
STACK_KEY64 (0x12) 64 位栈缓存键(相同用途)

栈跟踪附件通过 EVENT_ENABLE_PROPERTY_STACK_TRACEEVENT_FILTER_TYPE_STACKWALK 控制,内核栈捕获的注意事项在 §6.5 中详细说明。

7.3 正确识别 TraceLogging

一个广泛重复的传说称 TraceLogging 事件设置了关键字位 47。这是错误的,准确地说,位 47(0x0000800000000000)是 MICROSOFT_KEYWORD_CRITICAL_DATA,是三个 Microsoft 保留的遥测分类关键字之一(位 45 = TELEMETRY,位 46 = MEASURES,位 47 = CRITICAL_DATA)。它在 Microsoft 分类为关键遥测的广泛非 TraceLogging Manifest 事件上设置,在许多与遥测无关的 TraceLogging 事件上清除。

健壮的测试是 TraceLogging 元数据,而非关键字 47。在常见情况下,EVENT_HEADER.EventDescriptor.Channel == 11WINEVENT_CHANNEL_TRACELOGGING)标识正常的 TraceLogging 事件,因为 TraceLogging 默认使用通道 11。然而,在 Windows 10 及更高版本上,运行时也可以通过 Provider 特征 / EventSetInformation 将 Provider 标记为 TraceLogging 兼容,因此 TraceLogging 事件不要求永远使用通道 11。解析器应将通道 11 视为强提示,然后通过查找 TraceLogging Schema 元数据(EVENT_HEADER_EXT_TYPE_EVENT_SCHEMA_TL / Provider 元数据描述符)来确认。按关键字 47 对事件进行分类的代码既会遗漏 TraceLogging 事件,也会错误分类遥测事件;这是自制解析器中的常见 Bug。

7.4 磁盘上的 ETL

.etl 文件不是类似 EVTX 的数据库。它是刷新的 ETW Buffer 流。每个 Buffer 以 WMI_BUFFER_HEADER 开始;该头部之后的字节是变长事件记录序列。文件中的第一个记录通常是 WMI_LOG_TYPE_HEADER 系统事件:一个 SYSTEM_TRACE_HEADER,后跟原始 TRACE_LOGFILE_HEADER,后跟 Logger 名称和日志文件名字符串。OpenTrace 通过 EVENT_TRACE_LOGFILE.LogfileHeader 暴露该元数据的规范化版本。

对底层解析器重要的字段:

字段 实际含义
WMI_BUFFER_HEADER.BufferSize 磁盘上刷新的 Buffer 大小;实际上是到下一个 Buffer 的步长。
WMI_BUFFER_HEADER.SavedOffset / CurrentOffset Buffer 中有多少字节有效。
WMI_BUFFER_HEADER.SequenceNumber Session 内的单调 Buffer 顺序。
WMI_BUFFER_HEADER.ClientContext 该 Buffer 中记录的时钟/时间戳上下文。
TRACE_LOGFILE_HEADER.BootTime / PerfFreq / StartTime 将原始 QPC/周期时间戳转换为挂钟时间所需。
TRACE_LOGFILE_HEADER.EventsLost / BuffersLost 在 Logger 关闭或头部更新时捕获的文件级丢失证据。

跨 CPU 的事件排序是一个合并问题,而非链表遍历。Buffer 序列告诉你刷新顺序;事件时间戳告诉你逻辑时间;跨处理器的平局不稳定。ProcessTrace 尝试从最旧到最新交付事件,但 Microsoft 明确允许在时钟回退、两个 CPU 发出相同时间戳或记录损坏时无序交付。将回调顺序视为全序的取证解析器最终会产生虚假叙述。

损坏的最终 Buffer 不会毒害整个文件。通常的崩溃模式是,在撕裂写入之前的每个完整 Buffer 都能正确解析,最后一个 Buffer 在进度或边界检查时失败。直接解析器应按 Buffer 进行关闭失败处理:验证 BufferSize,验证每个事件至少前进其头部大小,在 SavedOffset 处停止,并保留之前的 Buffer。OpenTrace + ProcessTrace 已经处理了其中许多情况,但内存取证中使用的自定义解析器需要显式的进度检查。

7.5 压缩

从 Windows 8 开始(并通过 Windows 10 1607 SDK 中添加的 EVENT_TRACE_COMPRESSED_MODE 常量公开暴露),ETW 写入器可以在刷新到磁盘之前独立压缩每个 Buffer。重要的词是独立:压缩是每 Buffer 的转换,而非整个文件归档。撕裂写入或损坏的压缩块应只损失该 Buffer,而非整个 ETL。

XPRESS-Huffman 是现代有用的模式。在典型重型跟踪(System Trace Provider、CKCL、WPR CPU+磁盘配置文件)上的压缩比通常在 5-7 倍左右;CPU 开销集中在 Logger Worker 中,而非事件写入快速路径。wpr -compressxperf -CompressMode 2 都启用 XPRESS-Huffman;CompressMode 1 是较旧的 LZW 路径。OpenTrace 透明解压,因此普通 Consumer 不需要单独的代码路径。直接解析器需要。

压缩是文件模式功能。它不是减少实时 Consumer 压力的神奇方法,因为实时 Consumer 协议接收事件记录而非压缩的磁盘块流。如果设计需要实时检测和紧凑证据,使用实时 Session 进行检测,使用单独的压缩文件模式 Session 进行持久回放。

注意 EVENT_TRACE_COMPRESSED_MODE 是一个半文档化的日志模式常量:它出现在从 Windows 10 1607 SDK 开始的 EVNTRACE.H 头文件中,但不在公共"日志模式常量"表中。不同来源报告其数值(有些给出 0x04000000);安全做法是在可用时使用 SDK 头文件定义,而非硬编码。

8. Consumer

Consumer 是 ETW 从操作系统机制变成工程问题的地方。内核可以交付正确的事件,但缓慢的回调、Schema 缓存未命中、错误的时间戳假设或过度信任 UserDataLength 的解析器仍然可以使检测无用。

有两种消费模式:

模式 API 形式 操作形式
文件回放 OpenTrace(LogFileName) + ProcessTrace 最多 64 个 ETL 文件可以在一个 ProcessTrace 调用中合并。适用于取证和确定性回放。
实时 OpenTrace(LoggerName) 配合 PROCESS_TRACE_MODE_REAL_TIME 每个 ProcessTrace 调用一个实时 Session。适用于检测,但回调延迟成为背压。

实时 Consumer 创建一个 EtwConsumer 内核对象,将自己链接到 Session 的 Consumer 列表,并等待 Buffer 可用性。ProcessTrace 是阻塞的;生产模式是在专用线程上运行它,在 EventRecordCallback 中做最少的工作,并将解码或部分解码的记录移交给有界内部队列。如果回调格式化字符串、调用网络、接触数据库或同步上传证据,它就不再是 Consumer;它是针对自身 ETW Session 的拒绝服务面。

回调接收一个 EVENT_RECORD

struct EVENT_RECORD {
    EVENT_HEADER                     EventHeader;
    ETW_BUFFER_CONTEXT               BufferContext;   // processor #, logger ID
    USHORT                           ExtendedDataCount;
    USHORT                           UserDataLength;
    PEVENT_HEADER_EXTENDED_DATA_ITEM ExtendedData;
    PVOID                            UserData;
    PVOID                            UserContext;
};

Consumer 应围绕四条规则构建:

  1. 使用 PROCESS_TRACE_MODE_EVENT_RECORD 遗留的 EventCallback 格式丢失有用的上下文。EVENT_RECORD 携带扩展数据数组、Provider GUID、活动 ID、用户上下文和 TDH 期望的原始负载指针。

  2. 有意识地选择时间戳模式。 默认情况下,ProcessTrace 将时间戳转换为系统时间。PROCESS_TRACE_MODE_RAW_TIMESTAMP 保留 Provider/Session 时钟域,这更适合高分辨率关联和验证 QPC 漂移,但将转换推给 Consumer。

  3. 不要假设全序。 ProcessTrace 尝试按时间顺序交付,但跨 CPU 的相同时间戳、时钟调整和损坏记录可能产生无序回调。围绕窗口、进程启动密钥、活动 ID 和可用的单调每源序列构建检测器。

  4. BufferCallback 视为健康遥测。 它在 Buffer 处理后运行,给 Consumer 取消处理的机会。更重要的是,它是采样 BuffersRead、Buffer 填充和丢失证据的自然位置。实时 Consumer 应将"活着但正在丢失"与"死亡"分开报告。

TDH 是正确性粘合剂,而非免费的快速路径。TdhGetEventInformation 检索 EVENT_RECORD 的元数据;对于 WPP 和 Classic 事件,调用者可能需要 TDH_CONTEXT;对于 Manifest 事件,TDH 可能需要查找发布者注册并从 Provider 二进制文件加载 WEVT_TEMPLATE 资源。高吞吐量 Consumer 缓存以 (ProviderId, EventId, Version, Opcode, Task, Channel) 为键的结果,并在 Provider 二进制文件更改时失效。对于 TraceLogging,Schema 携带在事件负载/元数据中,因此在少量验证后直接解析自描述 Schema 通常更快更可靠。

TdhGetEventInformation 使用的元数据源优先级为:TraceLogging 负载 -> Manifest 发布者(HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\WINEVT\Publishers)-> WMI MOF 仓库 -> WPP(仅在有外部 TMF / PDB 时)。对于 Manifest 未注册的 Provider 的事件(例如随机 TraceLogging Provider),第二/三个源失败;Consumer 要么使用事件内 Schema,要么看到原始字节负载。

安全 Consumer 还需要解析器加固。EVENT_RECORD 中的每个长度和每个扩展数据项都应被视为不可信输入,尤其是在回放从另一主机获取的 ETL 文件时。在字段提取前验证 UserDataLength,验证每个 EVENT_HEADER_EXTENDED_DATA_ITEM.DataSize,拒绝零大小进度,并限制每事件分配。这不是理论上的:恶意 ETL 回放是卡住收集器或迫使其在每个事件上花费数秒进行元数据解析的简单方法。

9. Microsoft-Windows-Threat-Intelligence (ETWTI)

9.1 访问模型

ETWTI 是内核为源自内核内部且无法从用户态修补的安全相关事件而构建的专用通道。GUID {f4e1897c-bb5d-5668-f1d8-040f4d8dd344}。访问在启用/查询/消费时受到限制,有一个重要的 AutoLogger 细微差别:

  • 普通 Controller 路径: 调用 EnableTraceEx2 启用 Microsoft-Windows-Threat-Intelligence 的进程必须满足 Antimalware-PPL 访问检查。非 AM-PPL 调用者收到 ERROR_ACCESS_DENIED。在实践中,获得该身份通常意味着参与 Microsoft 反恶意软件/ELAM 签名路径,但内核执行的检查是保护/签名者级别,而非 Provider 上的通用"ELAM 签名调用者"位。

  • AutoLogger 路径: AutoLogger Provider 启用由内核在启动期间执行,而非由用户态 Controller 身份执行。当 AutoLogger 请求 ETWTI 或 Microsoft-Windows-Kernel-Audit-API-Calls 时,Session 被标记为 SecurityTrace 标志,因此后续的查询/消费操作应限制为 AM-PPL 调用者。最近的公开研究表明此边界存在边缘情况,包括本机 API 消费和停止跟踪行为并不总是应用与高级 API 相同的检查。

  • 状态跟踪: ETW_SILODRIVERSTATE.EtwpSecurityLoggers[8] 跟踪消费安全 Provider 系列的 Logger 槽位,WMI_LOGGER_CONTEXT.Flags.SecurityTrace 标记受保护的 Session。将 EtwpSecurityProviderPID 视为实现细节,而非通用的"唯一 ETWTI Consumer PID"真相;多个安全 Session 和 AutoLogger 路径可以共存。

此处重要的运行时身份是 PsProtectedSignerAntimalware 加上 PPL-light 保护类型。Microsoft 的受保护反恶意软件服务模型不是"管理员加证书";它是一个引导链:

  1. 供应商安装 ELAM 驱动。
  2. ELAM 驱动携带一个资源节,注册允许签名用户态反恶意软件服务及其非 Windows DLL 的证书哈希。
  3. 服务配置为 SERVICE_CONFIG_LAUNCH_PROTECTED / SERVICE_LAUNCH_PROTECTED_ANTIMALWARE_LIGHT
  4. 在服务启动时,SCM 和代码完整性验证注册的证书信息并作为受保护服务启动。
  5. 在内核中,进程保护字节解析为具有反恶意软件签名者的受保护轻量级进程。Microsoft 自己的调试示例显示 _EPROCESS.Protection.Level = 0x31, Type = 0y0001, Signer = 0y0011 用于反恶意软件受保护服务。

这就是为什么普通管理员、以 LocalSystem 运行的普通服务甚至许多其他 PPL 级别不能像普通 Provider 一样打开 ETWTI。访问检查不由 SeDebugPrivilege 满足;它由受保护进程的签名者/保护级别满足。ELAM 是注册和证书注册路径。PPL-AM 是 ETWTI 访问路径关心的运行时属性。

操作后果:

场景 ETWTI 结果
使用 EnableTraceEx2 的普通管理员 Controller 在普通路径上返回 ERROR_ACCESS_DENIED
没有 AM-PPL 的 LocalSystem 服务 仍然被拒绝;令牌权限不够
Microsoft Defender / MDE AM-PPL 组件 可以拥有或代理安全跟踪
具有有效 ELAM + 受保护服务注册的第三方 AV 如果以 AM-PPL 启动则有资格
没有 AM-PPL 身份的反作弊 不能依赖直接用户态 ETWTI 消费;使用签名的内核组件、AM-PPL 代理或重新发射/测试架构
如 SealighterTI 的研究重发射驱动 可以在实验室中包装 ETWTI,但创建了新的信任边界

调试访问失败时,检查两侧。用户态应验证服务启动保护和签名者注册;内核/调试器检查应验证 _EPROCESS.Protection 而不仅仅是镜像名称。在 ETW 侧,查询 Session 的 SecurityTrace 标志和 EtwpSecurityLoggers[] 槽位;存在但被非 AM-PPL 进程消费的安全跟踪 Session 要么使用边缘情况、代理路径,要么是需要实时验证的陈旧状态。

这就是为什么编写自己的 ETWTI Consumer 是一个有意义的工程项目。你需要 AM-PPL 合格的服务/驱动路径,或者需要一个签名驱动来包装 ETWTI 并在标准 Provider 上重新发射事件,这就是 SealighterTI 为研究环境使用的架构。

9.2 Provider 元数据(Win11 25H2 build 26200.8524)

当前 25H2 Provider 元数据与 ETW 研究笔记中流传的旧有序事件 ID 表不匹配。在此版本上,wevtutil gp Microsoft-Windows-Threat-Intelligence /ge:true /gm:true 报告以下任务/事件/关键字形状:

行为族 事件 ID 主要关键字 安全用途
远程可执行分配 1 ALLOCVM_REMOTE (0x4) 分配执行注入
本地可执行分配 6 ALLOCVM_LOCAL (0x1) 可疑本地 RX/RWX 暂存
内核调用者的可执行分配 21 / 26 ALLOCVM_REMOTE_KERNEL_CALLER (0x8), ALLOCVM_LOCAL_KERNEL_CALLER (0x2) 驱动辅助的用户态暂存
远程可执行保护更改 2 (版本 1-3) PROTECTVM_REMOTE (0x40) 分配后保护注入
本地可执行保护更改 7 (版本 1-3) PROTECTVM_LOCAL (0x10) 本地 JIT/stager 区分
内核调用者的可执行保护 22 / 27 (版本 1-3) PROTECTVM_REMOTE_KERNEL_CALLER (0x80), PROTECTVM_LOCAL_KERNEL_CALLER (0x20) BYOVD / 辅助驱动暂存
远程可执行节映射 3 MAPVIEW_REMOTE (0x400) 节映射注入
本地可执行节映射 8 MAPVIEW_LOCAL (0x100) 本地 RX 镜像/节映射
内核调用者的节映射 23 / 28 MAPVIEW_REMOTE_KERNEL_CALLER (0x800), MAPVIEW_LOCAL_KERNEL_CALLER (0x200) 驱动辅助节映射
远程用户 APC 队列 4 QUEUEUSERAPC_REMOTE (0x1000) APC 注入
内核调用者的 APC 队列 24 QUEUEUSERAPC_REMOTE_KERNEL_CALLER (0x2000) 内核辅助 APC 交付
远程设置线程上下文 5 SETTHREADCONTEXT_REMOTE (0x4000) 线程劫持
内核调用者的设置线程上下文 25 SETTHREADCONTEXT_REMOTE_KERNEL_CALLER (0x8000) 驱动辅助劫持
VM 读取 11 本地, 13 远程 READVM_LOCAL (0x10000), READVM_REMOTE (0x20000) LSASS / 游戏进程内存读取
VM 写入 12 本地, 14 远程 WRITEVM_LOCAL (0x40000), WRITEVM_REMOTE (0x80000) 跨进程代码/数据注入
线程挂起/恢复 15 / 16 SUSPEND_THREAD (0x100000), RESUME_THREAD (0x200000) EDR/反作弊线程篡改
进程挂起/恢复/冻结/解冻 17 / 18 / 19 / 20 SUSPEND_PROCESS, RESUME_PROCESS, FREEZE_PROCESS, THAW_PROCESS (0x400000..0x2000000) 整个进程篡改和生命周期滥用
驱动/设备事件 29 / 30 / 31 / 32 DRIVER_EVENTS (0x40000000), DEVICE_EVENTS (0x80000000) 驱动和设备对象拓扑
进程模拟 33 / 34 / 36 PROCESS_IMPERSONATION_UP, PROCESS_IMPERSONATION_REVERT, PROCESS_IMPERSONATION_DOWN 令牌级权限移动
进程系统调用使用 35 PROCESS_SYSCALL_USAGE (0x10000000000) 可疑系统调用配置文件状态

关键字目录比上述事件行更广。当前 26200 元数据还暴露了 CONTEXT_PARSEEXECUTION_ADDRESS_VAD_PROBEEXECUTION_ADDRESS_MMF_NAME_PROBEREADWRITEVM_NO_SIGNATURE_RESTRICTION、读/写/保护事件的 VAD 填充关键字以及 QUEUEUSERAPC_AT_DPC。其中一些关键字在此主机上未附加到不同的公共 Manifest 事件行,因此在针对目标 Manifest/PDB 验证之前,应将它们视为版本特定的门或版本化的负载提示。

两个更正在操作上很重要。首先,将事件 ID 8 标记为 SetThreadContext、ID 16 标记为 DuplicateHandle 或 ID 21 标记为 CET 审计的旧笔记对当前 26200 元数据是错误的:事件 8 是本地可执行映射视图,事件 16 是线程恢复,事件 21 是远程内核调用者分配 VM。其次,ETWTI 不是每个句柄或 CET 信号的正确来源。对于句柄打开/复制,将 Microsoft-Windows-Kernel-Audit-API-CallsObRegisterCallbacks 配对使用;对于 CET/影子栈异常,使用平台的 CET/审计表面和栈完整性遥测,而非假设 ETWTI 事件 ID。

确切的按版本可用性必须通过 logman query providers Microsoft-Windows-Threat-Intelligencewevtutil gp Microsoft-Windows-Threat-Intelligence /ge:true /gm:true 或每版本 Manifest 存档进行检查。消费 ETWTI 的防御者应至少包含远程读取关键字并按目标进程过滤结果事件。任何从 LSASS 或受保护游戏进程读取的非调试器进程都是高置信度的内存窃取信号。

9.3 无法从用户态绕过

因为 ETWTI 事件从用户态攻击者必须调用来执行底层操作的内核例程内部发出。用户态调用者不能在不最终通过系统调用进入 MiReadWriteVirtualMemory 的情况下写入另一个进程的地址空间。任何数量的用户态修补都无法禁用它们。(已经拥有任意内核写入的攻击者当然可以完全绕过这些例程;该情况属于下面的 DKOM。)在实践中击败 ETWTI 需要以下之一:

  1. 内核写入(BYOVD -> 对 ETWTI 注册句柄或 ProviderEnableInfo.IsEnabled 的 DKOM)。这是 Lazarus FudModule 的方法。
  2. 击败 Consumer 或交付路径(饿死 Session、停止配置错误的安全跟踪、在内核帮助下挂起 PPL-AM Consumer 或利用访问控制边缘情况)。
  3. 避免触发事件的内核路径(例如使用 NtContinue 在当前线程上设置线程上下文,这不遍历 PspSetContextThreadInternal,因此不发出 EtwTiLogSetContextThread)。这是 §11 中涵盖的 Patchless 类别。

9.4 AMSI Provider 链和 Defender 的 ETW 架构

ETWTI 处理内核驻留行为信号。用户态脚本和内容等价物是 AMSI(Antimalware Scan Interface,反恶意软件扫描接口),它从两侧连接到 ETW。AMSI 发出 Consumer 读取的事件,AMSI 内部使用 ETW 进行其自身的内部关联。理解两个方向很重要,因为 Windows 平台上最受攻击的检测路径经过这里。

9.4.1 AMSI 架构

AMSI 是一个代理模式。应用宿主(PowerShell、WSH、Office、Edge、IIS、.NET)在即将执行或渲染的内容上调用 AmsiScanBuffer / AmsiScanString。AMSI 运行时遍历在 HKLM\SOFTWARE\Microsoft\AMSI\Providers\{CLSID} 下注册的 Provider 并将扫描请求分发给每个。Defender 捆绑的 Provider 是 MpOav.dll(CLSID {2781761E-28E0-4109-99FE-B9D127C57AFE});第三方 AV/EDR 在同一键下注册自己的 Provider。

Application (PowerShell, etc.)
   │  AmsiScanBuffer(buffer)
   ▼
amsi.dll  ──>  load each provider CLSID
   │
   ├─ MpOav.dll (Defender)  ──ALPC──>  MsMpEng.exe  ──cloud-detonate──>  result
   ├─ ThirdParty.dll                    │
   └─ ...                               ▼
                                  AMSI_RESULT_*
   ▼
Application acts on result (run / block)

MpOav.dll 通过私有 ALPC 通道与 Defender 引擎通信(确切端口名称是内部的,且在 Defender 平台版本间已更改,因此生产工具不应依赖它);MsMpEng.exe 以 PPL-AM(PsProtectedSignerAntimalware)运行,因此其内存和令牌受到保护,防止普通管理员篡改。Defender 引擎执行签名匹配、ML 推理以及(当 MAPS 云保护启用时)云沙箱引爆,返回 AMSI_RESULT_CLEANAMSI_RESULT_NOT_DETECTEDAMSI_RESULT_DETECTEDAMSI_RESULT_BLOCKED_BY_ADMIN 之一。

9.4.2 ETW 发射

独立于扫描结果,amsi.dll 在每次扫描时发出一个 ETW 事件:

  • Provider: Microsoft-Antimalware-Scan-Interface ({2a576b87-09a7-520e-c21a-4942f0271d67})
  • 事件 ID 1101 - AmsiScanBuffer。负载:Session GUID、Buffer 的 SHA-256(Hash 的前 32 字节)、ContentSizeContentName(例如"PowerShell"、"VBScript"、"JScript"、"Office_VBA")、AppName(调用宿主进程名)、ScanResult、以毫秒为单位的持续时间。

事件 1101 是 SOC 最有用的单一 AMSI 信号,因为 SHA-256 让防御者即使在内容本身未保留的情况下也能跨主机关联扫描内容。不要假设存在更广泛的公共 AMSI 事件集而不检查目标 Manifest。在本文使用的实时 26200 主机上,wevtutil gp Microsoft-Antimalware-Scan-Interface /ge:true /gm:trueamsi.dll 中仅暴露事件 1101(版本 0 和 1);提及额外 AMSI 生命周期或 UAC 扫描事件的旧笔记和私有 Provider 跟踪应在验证之前被视为版本/Provider 特定的。

9.4.3 Defender 自身的 Provider

Defender 向 MDE SOC 需要了解的多个 Provider 发出事件。以下 GUID 来自公开的第三方研究,部署前应在目标版本上使用 logman query providers <name> 重新验证。Microsoft 不发布 AM Provider 的规范 Manifest。

Provider GUID(使用前验证) 用途
Microsoft-Antimalware-Engine {0a002690-3839-4e3a-b3b6-96d8df868d99} 扫描开始/完成、签名匹配、NRI/ML 推理、修复;Defender 引擎自身的行为遥测
Microsoft-Antimalware-Service {751ef305-6c6e-4fed-b847-02ef79d26aef} MpSvc.dll / MsMpEng.exe 服务生命周期事件
Microsoft-Windows-WindowsDefender {11cd958a-c507-4ef3-b3f2-5fd9dfbd2c78} Microsoft-Windows-Windows Defender/Operational 下的 Operational 通道事件 - 大多数公共 Sigma 规则中使用的事件 1006/1007/1015/5007
Microsoft-Antimalware-AMFilter {cfeb0608-330e-4410-b00d-56d8da9986e6} MpFilter.sys 微过滤器 pre/post-create 回调(过滤器高度在 AV 范围约 328000 处,使用 fltmc instances 验证确切值)

Microsoft-Antimalware-Engine 不是"安全"Provider。任何管理员都可以订阅,这使它成为在不通过 EventLog 通道的情况下实时查看 Defender 对事件看法的经典位置。

9.4.4 超越 AmsiScanBuffer 修补的 AMSI 绕过

经典的 AmsiScanBuffer 首字节修补已广为人知;现代绕过直接针对 AMSI 内部状态,因为 API 表面更难监控字节变化:

  • AMSI 上下文损坏。 AMSI 维护一个每 Session 的 HAMSICONTEXT(一个在结构开头对 ASCII 标记"AMSI"进行内部验证的不透明句柄)。损坏宿主应用(例如 PowerShell)持有的上下文中的验证标记——而不触碰 amsi.dll 代码字节——会导致后续 AmsiScanBuffer 调用验证失败并在不扫描的情况下返回。S3cur3Th1sSh1t、Rastamouse 等均有文档记录。确切字段名(g_amsiContext 与宿主进程本地上下文句柄)在不同文章中有所不同。

  • CLR_AMSIContext / .NET 内存内 AMSI 绕过。 CLR(clr.dll / coreclr.dll)维护自己的 AMSI 上下文,当通过 Assembly.Load(byte[]) 加载 .NET 代码时使用。将静态字段替换为空操作存根可以绕过内存内 .NET 负载的 AMSI(xpn、Flangvik)。

  • 硬件断点 + VEH(HellsHollow / AceLdr)。与无补丁 ETW 绕过(§11.3)相同的原语应用于 AMSI:将 Dr0 设置为 AmsiScanBuffer,安装 VEH,强制返回 AMSI_RESULT_CLEAN。绕过和无补丁 ETW 绕过通常在同一个加载器中组合,因为目标是共置的。

  • 进程内上下文重建。 某些 PoC 使用新的 amsi.dll!CAmsiAMScan::Init 调用重新初始化 AMSI 上下文,将上下文指向受控的 Provider 列表,有效地将 Defender 替换为攻击者控制的扫描器。需要比字节翻转更多的代码,但不留下内联补丁。

检测信号:AMSI 绕过尝试几乎总是可以在 Microsoft-Windows-PowerShell 的 ScriptBlock(事件 4104)中看到,因为绕过本身是必须在修补任何东西之前通过 ScriptBlock 编译的 PowerShell。组合启发式查找最近的 4104 事件,包含如 etwProviderAmsiUtilsg_amsiContextMpOav[Ref].Assembly.GetType 的字符串,随后同一运行空间中 4104/1101 事件的持续缺失。这是 AMSI/ETW 用户态绕过的最高置信度单一信号。

10. Hooking ETW:InfinityHook 及其继承者

10.1 经典 InfinityHook:直接 GetCpuClock 指针交换

经典 InfinityHook 方法针对 CKCL,因为 (a) 它始终在运行,(b) 它可以配置为记录系统调用。在系统调用入口路径上,KiSystemCall64 调用 PerfInfoLogSyscallEntry,最终通过 Logger 的时钟源为 ETW 记录添加时间戳。在旧版本上,相关的 WMI_LOGGER_CONTEXT.GetCpuClock 成员是一个可调用的函数指针。将其交换为攻击者控制的函数将每个捕获的系统调用变成一行 Hook:

ULONG64 OriginalGetCpuClock;

ULONG64 HookedGetCpuClock(void) {
    PKTHREAD t = KeGetCurrentThread();
    ULONG syscallNum = *(PULONG)((UCHAR*)t + KTHREAD_SYSTEMCALLNUMBER_OFFSET);

    if (syscallNum == NTPROTECTVIRTUALMEMORY_NUMBER) {
        // intercept here - argument frame is in the caller's stack
    }

    return OriginalGetCpuClock();
}

该机制可以从一个内核驱动到达,该驱动能找到 CKCL 的 WMI_LOGGER_CONTEXT(遍历 PspHostSiloGlobals -> EtwSiloState -> EtwpLoggerContext[],或使用调试器/PDB 派生的 ETW 调试器数据,然后查找"Circular Kernel Context Logger"名称和 CKCL 实例 GUID)。攻击者还必须配置 CKCL 的 EnableFlags,使所需的热事件类实际被发出。这通常通过支持 StartTraceControlTrace 的同一个 NtTraceControl/ZwTraceControl 系列来完成,内部 WMI_LOGGER_INFORMATION 携带 Logger 名称、实例 GUID、标志和模式。

必须精确的一点:直接指针交换是旧原语,而非整个 InfinityHook 家族。

10.2 索引修补并未终结该家族

Microsoft 的缓解措施将 GetCpuClock 从原始函数指针改为小型时钟源选择器。这打破了直接的"将我的内核指针写入 GetCpuClock"方法。这并不意味着 ETW 不再可用作 Hook 表面。

Windows 11 24H2 的公共逆向工程显示现代调度大致如下:

GetCpuClock 值 观察到的时间戳路径 Hook 含义
0 RtlGetSystemTimePrecise() 普通系统时间路径;不是常见的 Hook 分支。
1 KeQueryPerformanceCounter() 到达 HAL 注册的定时器/性能计数器后端;现代 InfinityHook 式工作集中在这里。
2 HalPrivateDispatchTable.HalTimerQueryHostPerformanceCounter() 较旧的备用 HAL 调度路径;保护和可行性因版本而异。
3 __rdtsc() 快速周期计数器路径;此分支中没有明显的可写回调。
> 3 快速失败 / Bugcheck 用作损坏信号,而非 Hook。

这是对常见神话的纠正。Windows 11 22H2/24H2/25H2 时代的内核不允许你直接将任意函数指针存储在 GetCpuClock 中,但具有内核写入权限的 Controller 仍然可以强制 CKCL 进入到达可变下游回调的时钟模式。Anze Lesnik 在 Windows 10 20H1 时间框架描述了索引后思路;Denis Skvortsov 在 Windows 11 22H2 Avast 自防御分析中展示了同一家族;Archie 的 2025 文章在 Windows 11 24H2 上测试了上下文交换变体。

10.3 现代 HAL 定时器 / QueryCounter Hook

现代分支是:

ETW event write
  -> EtwpReserveTraceBuffer / EtwpLogKernelEvent
      -> LoggerContext->GetCpuClock == 1
          -> KeQueryPerformanceCounter()
              -> hal!_REGISTERED_TIMER.FunctionTable.QueryCounter(...)
                  -> attacker hook

权衡在于 KeQueryPerformanceCounter 不是 ETW 专用的。它是整个内核和驱动中使用的极其热的时间原语。一个工作的 Hook 因此必须区分"从 ETW 为我关心的 Logger 调用"和普通 QPC 调用。公共 24H2 逆向工程报告 ETW 源路径在 r15 中有一个指向 Logger Context 的指针;较旧或不同的版本可能需要扫描当前栈以查找合理的 WMI_LOGGER_CONTEXT 指针。这正是一种生产防御者应使用符号和实时测试验证而非从博客文章复制的每版本假设。

对于系统调用拦截,目标仍然是与经典 InfinityHook 相同的概念帧:PerfInfoLogSyscallEntry 在最终 Nt* 处理程序被调用之前运行,处理程序指针或足够的系统调用上下文在测试版本上仍然可以从栈/寄存器状态恢复。Hook 不是"使 ETW 失明";它使用 ETW 的时间戳侧路径在 ETW 记录系统调用时运行代码。

检测模型从"GetCpuClock 是内核指针吗?"变为:

  • CKCL 是否意外配置为针对相关事件类通过 QPC/HAL 路由的时钟模式?
  • hal!_REGISTERED_TIMER.FunctionTable.QueryCounter 是否解析到预期的 hal.dll/ntoskrnl.exe 镜像范围?
  • 指针在启动和 CPU 电源管理转换期间是否稳定?
  • Hook 内的调用者过滤器是否依赖防御者可以在实验室版本中采样的 ETW 特定寄存器或栈工件?

最强的工件不是 GetCpuClock == 1 本身;那可以是合法的。更强的工件是 HAL 定时器回调指向已签名 HAL/nt 镜像范围之外,同时 CKCL 配置为通过该分支路由高频事件。

10.4 ETW 上下文交换 Hook

2025 年的上下文交换变体将目标事件从系统调用入口移到调度器转换。CKCL 可以发出上下文切换事件,ETW 路径在预留和添加时间戳之前到达 EtwpLogContextSwapEvent。在测试的 24H2 版本上,旧/新线程指针仍然可以在时间戳 Hook 运行之前从寄存器或函数序言的保存副本中恢复。

这给驱动一个在每个上下文切换上的回调,而无需修补调度器、SSDT 或 KiSwapContext

scheduler transition
  -> ETW context-switch emission
      -> EtwpLogContextSwapEvent
          -> EtwpReserveTraceBuffer timestamp path
              -> HAL QueryCounter hook

这比系统调用 Hook 更广泛。它可以观察从不涉及用户态系统调用边界的线程转换,并且可以防御性或攻击性地使用:反作弊可以检查传出线程的栈以查找未签名执行,而作弊可以使用转换来为选定线程交换页表或内存可见性状态。它也更脆弱:它在调度器敏感的时序运行,容易受到 IRQL/性能错误的影响,并且依赖于 EtwpLogContextSwapEvent 周围的确切寄存器/栈布局。

10.5 防御要点

正确的 2026 年声明是:

  1. 经典的 GetCpuClock 直接指针 InfinityHook 在现代版本上已消失。
  2. InfinityHook 风格的 ETW 热路径 Hook 通过移动到下游时间戳后端和其他 ETW 发出的热事件(如上下文切换)而存活。

对于防御者,这意味着内核 ETW 验证器必须包括时钟后端,而不仅仅是 Logger Context。验证 WMI_LOGGER_CONTEXT.GetCpuClock、CKCL EnableFlags、使用的 HalPrivateDispatchTable 定时器回调、hal!_REGISTERED_TIMER.FunctionTable.QueryCounter,以及这些指针是否落在受信任的镜像范围内。如果启用了 KDP/HVCI,还要验证相关定时器/调度页面在实际版本上是否受保护;不要因为机器"VBS 开启"就假设它们受保护。

11. 篡改 ETW:从用户态补丁到 BYOVD DKOM

11.1 用户态补丁(ntdll!Etw*)

最简单的绕过:将 ntdll!EtwEventWritentdll!NtTraceEvent 的首字节修补为 C3(ret),或者更微妙一些,将 EtwEventEnabled 修补为 33 C0 C3xor eax, eax; ret),使 Provider 认为自己被禁用并跳过事件写入路径。NtTraceEvent 本身也存在变体,效果相同。

这些补丁仅影响被修补的进程。它们不触及内核 ETW Provider。Microsoft-Windows-Kernel-Process 继续从内核内部发出进程创建事件,无论用户态 ntdll 被多么激进地修补,因为内核侧直接调用 EtwWrite。因此:

  • 用户态 ETW 补丁确实可以绕过单个 PowerShell 实例内的 Microsoft-Windows-PowerShell 事件 4104,因为写入者位于用户态。
  • 它们不能绕过 Microsoft-Windows-Kernel-Process 事件 1(进程创建),因为写入者位于内核。

一个适度的 EDR 检测:定期验证自身进程中 EtwEventWrite 的首字节与已知良好的序言是否匹配。其他任何情况都是篡改。

11.2 攻击者获得内核写入后的内核 DKOM

有了 BYOVD 或内核漏洞利用,每个 ETW 状态字节都变得可写。最小的可能原语:

// (1) Nullify the ETWTI registration handle in ntoskrnl.exe .data
*(PVOID*)EtwThreatIntProvRegHandle = NULL;
// -> every EtwTiLog* call sees a NULL handle and short-circuits via its
//   own NULL check (or via the EnableMask test inside EtwpWriteUserEvent)

// (2) Disable a specific provider
GuidEntry->ProviderEnableInfo.IsEnabled = 0;

// (3) Disable one enable slot. The array index is 0..7, not LoggerId.
GuidEntry->EnableInfo[slot].IsEnabled = 0;

// (4) Starve one slot by using an impossible keyword for this provider.
// MatchAnyKeyword = 0 usually means "match all", so do not use zero.
GuidEntry->EnableInfo[slot].MatchAnyKeyword = ImpossibleKeywordBit;

// (5) Clear a registration's session-bitmask
RegEntry->EnableMask = 0;

每个原语在 1 到 8 字节之间,在纳秒内运行。它们都不产生如"ETW Session 终止"之类的嘈杂工件;它们只是使路径静默。检测它们的唯一方法是 (a) 与 Controller 预期状态的一致性交叉检查,(b) KDP/HVCI 使底层页面不可写,或 (c) PatchGuard/KPP 最终在被触碰字段位于其采样集的版本上注意到受保护状态损坏。不要依赖 PatchGuard 作为确定性检测器;它是随机的、版本特定的且有意未充分文档化的。

Lazarus 的 FudModule 是 (1) 针对特定 ETWTI 的经典实现:扫描 ntoskrnl!.text 中的 call EtwRegister 站点,回溯到将结果句柄存储到全局变量的参数,跟随全局变量到其 .data 地址,写入 NULL。该技术是通用的。它适用于每个注册句柄最终位于可写内核 .data 中的 Provider,基本上所有 Provider 都是如此。

一个特别粗暴的变体是将零写入 EtwpActiveSystemLoggers,即当前运行系统 Logger 的位掩码,一举退役 NT Kernel Logger 和 CKCL。信号足够严重,即使是业余防御者也会注意到;FudModule 家族中更复杂的实现选择性地仅清零攻击者不喜欢的 Provider(ETWTI、Kernel-Process)的 ETW 句柄,而保留其余部分,以便空闲流量统计继续流动。

11.3 无补丁绕过:硬件断点 + VEH

一旦 EDR 开始验证 ntdll!Etw* 字节完整性,攻击性研究转向完全不修改内存。Turla "Kazuar v3" 恶意软件(2023 年由 Lab52 / Sentinel Labs 分析公开报告,后续开源 PoC 实现在 2024 年完善了该技术)是说明性的:

1. GetThreadContext on the current thread.
2. Set Dr0 = NtTraceEvent_addr, Dr1 = AmsiScanBuffer_addr, Dr7 to enable both.
3. NtContinue(&ctx, FALSE)      <- NOT SetThreadContext.
4. AddVectoredExceptionHandler(1, MyVEH).
5. MyVEH on EXCEPTION_SINGLE_STEP at NtTraceEvent:
     adjust ctx->Rip to a RET, set Rax=STATUS_SUCCESS;
     return EXCEPTION_CONTINUE_EXECUTION.

两个设计选择证明了这种笨拙的合理性:

  • NtContinue 而非 SetThreadContext - SetThreadContext 流经 PspSetContextThreadInternal,该路径发出 ETWTI 设置线程上下文事件(SETTHREADCONTEXT_REMOTE,当前 26200 元数据上的事件 ID 5)。NtContinue 直接将上下文恢复到调用线程上,不遍历该路径,因此不会触发跨线程 ETWTI 设置上下文事件。权衡是 NtContinue 仅适用于当前线程。

  • Vectored 异常处理程序而非内联修补 - 硬件断点方法使目标指令保持不变。对函数序言进行哈希检查的 EDR 看不到任何问题;只有检查每个线程上的 Dr0..Dr7 才能揭示该技巧。

防御者可以利用的局限性:x86/x64 只有四个调试寄存器,断点是每线程的(同一进程生成的新线程获得 Dr0..Dr7 = 0),VEH 注册可通过 RtlpCallVectoredHandlers 插桩或通过 Microsoft-Windows-Kernel-ProcessKERNELBASE.dll 镜像加载事件后跟 RIP 启发式来观察。White Knight Labs 的 LayeredSyscall(2024)通过使用两层 VEH 进一步提升了这一点。第一层处理攻击者自己的单步异常,第二层是诱饵链,用于混淆遍历 RtlpCallVectoredHandlers 的 EDR。

11.4 参数欺骗绕过(TamperingSyscalls 及其继承者)

TamperingSyscalls(rad9800,2022-2024)针对的不是 ETW 本身,而是用户态 EDR 从 ETW 事件构建的故事。该技术:

1. Caller builds a "benign" argument frame on the stack.
2. Sets a hardware breakpoint at the syscall instruction inside the ntdll stub.
3. VEH fires immediately before SYSCALL: rewrites the argument registers
   (RCX, RDX, R8, R9) and the stack to the malicious values.
4. SYSCALL executes with the real, dangerous arguments.
5. Kernel performs the operation and emits ETW/ETWTI events with the real values.
6. On return, VEH restores the original "benign" frame so any post-call
   user-mode telemetry sees the lie.

该技术干扰了通过跟踪系统调用存根或镜像其参数到用户态记录器来推断事件语义的 EDR。内核 ETWTI 不受影响。EtwTiLogProtectExecVmMiProtectVirtualMemory 内部发出,此时内核已接收真实参数,因此事件负载包含实际的 PAGE_EXECUTE_READWRITE 掩码。因此绕过仅伤害检测逻辑位于用户态或与用户态参数捕获关联的 EDR。

检测方法:将 ETWTI 的内核侧负载与同一系统调用的任何用户态"影子"进行关联并在分歧时告警;或 Hook IDT #DB 处理程序以观察系统调用边界的硬件断点活动;或定期扫描每个线程上的 Dr0..Dr7(与无补丁 ETW 绕过相同的防御)。

11.5 RPC Provider 绕过(rpcrt4!_g_fProfile)

Microsoft-Windows-RPC ({6ad52b32-d609-4be9-ae07-ce8dae937e39}) 发出带有接口 UUID 属性的调用事件,横向移动检测规则(IRemoteWinspoolSvcITaskScheduler2lsarpc 凭据转储、DRSUAPI / DCSync)依赖于这些事件。在 rpcrt4.dll 内部,一个从 EtwEventEnabled 派生的进程本地启用缓存控制 Provider 是否发出事件(确切符号名因版本而异。公开符号之前的文献将其称为 _g_fProfile,但规范机制是 ntdll/rpcrt4 在首次 EtwEventEnabled 调用后缓存的每进程 Provider 状态)。绕过方式:

// (cross-process variant) - kill RPC telemetry inside the target server
PVOID target = GetProcAddress(GetRpcrt4InRemoteProcess(pid),
                               "rpcrt4_g_fProfile_or_equivalent");
ULONG zero = 0;
WriteProcessMemory(hRemote, target, &zero, sizeof(zero), NULL);
// the inbound RPC server no longer emits ETW events for any call

效果是进程本地的。系统其余部分的 RPC 遥测继续,但它从攻击者正在穿越的特定服务器(例如 lsass.exespoolsv.exeservices.exe)中移除了入站调用归因。检测需要将内核侧信号与同一时间窗口内预期 Microsoft-Windows-RPC 事件的缺失进行关联:RPC 端口上的 Microsoft-Windows-Kernel-Network TCP 接收、命名管道传输的 Microsoft-Windows-Kernel-File 命名管道创建,以及针对 lsass.exeMicrosoft-Windows-Kernel-Audit-API-Calls NtOpenProcess

11.6 调用栈欺骗

一旦 EDR 通过 EVENT_FILTER_TYPE_STACKWALK 收集栈,下一步就是让这些栈撒谎。三种公开文档化的技术被广泛采用:

  • SilentMoonwalk / klezVirus. 通过 JOP 链穿过 __C_specific_handler 来伪造一个假的 RUNTIME_FUNCTION 链,从而使栈不同步。ETW 的栈遍历产生指向 ntdll!RtlUserThreadStart 的归因,而不是恶意帧。击败了每个依赖 .pdata 进行展开的遍历器。

  • CallStackSpoofer / VulcanRaven (mgeeky). 推送合成帧并通过 RtlAddFunctionTable 注册攻击者提供的展开信息。ETWTI 捕获的链归因于展开信息命名的任何良性函数。

  • 内核侧变体滥用 KeUserModeCallback 来污染内核栈,使得 RtlWalkFrameChain 返回截断的帧。 生产环境反作弊驱动(在 Vanguard 中公开可见)对捕获帧之间的 RSP 增量与实际栈页布局进行完整性检查,并标记不可信的较大增量。

系统性防御是 Intel CET 影子栈。在启用 CET 的情况下(Tiger Lake+ CPU,有资格使用 24H2/25H2 SystemGuard 的机器),每个被欺骗的返回地址都可能与影子栈不匹配,产生 #CP 异常或通过 CET/平台遥测产生审计信号。不要将其硬编码为 ETWTI 事件 ID 21;在当前 26200 元数据上,ID 21 是一个 alloc-VM 内核调用者事件。在不支持 CET 的硬件上,实用的回退方案是将任何以 ntdll!RtlUserThreadStart 结尾且没有可信内部帧的栈,或任何帧间页面增量小得不可信的链,视为值得升级到内存扫描的篡改信号。

11.7 ProcessInstrumentationCallback 重定向

NtSetInformationProcess(ProcessInstrumentationCallback) 安装一个每进程回调,内核在每次系统调用返回到用户态时调用该回调。最初是 Wow64 转换设施,该原语已被充分文档化,以至于攻击性使用现在已成为标准:

PROCESS_INSTRUMENTATION_CALLBACK_INFORMATION info = {0};
info.Callback = my_user_mode_callback;   // 内核跳转到的 RIP
info.Version  = 0;
NtSetInformationProcess(GetCurrentProcess(),
                        ProcessInstrumentationCallback,
                        &info, sizeof(info));

安装后,当内核在任何系统调用结束时返回到用户态时,控制权跳转到 my_user_mode_callback 而不是恢复原始返回地址;原始 RIP 通过 CPU 寄存器(在公开文档化的 PoC 中 x64 上为 R10,尽管精确的调用约定是内部的且未正式文档化)传递给回调,回调被期望在运行后跳转回该地址。该回调可以检查或修改返回值、门控哪些系统调用产生"可见"效果,或重写事件发射状态。针对 ETW 的两种不同攻击性用途:

  • 抑制事件发射,通过检测系统调用返回并在任何用户态副作用之前短路。由于回调在内核已经发出其 ETW 事件后运行,这不会击败 ETWTI 或内核 ETW——这些事件已经在缓冲区中。然而,它确实击败了 EDR 用户态组件尝试的任何用户态"系统调用后"遥测。

  • 欺骗返回值,以击败一致性检查。

Elastic Security Labs 的 2023 年检测内容明确查找异常的 ProcessInstrumentationCallback 安装,因为它是一个文档化的 EDR 规避原语——几乎没有良性软件会安装它。

内核将回调指针保存在 EPROCESS.InstrumentationCallback 中。检测方法:通过 ETWTI 关键字监视 NtSetInformationProcess 调用者,或对每个进程采样 EPROCESS.InstrumentationCallback 并在非 NULL 值(超出 Wow64-on-x64 仿真集的范围)时发出告警。

11.8 过滤器反转篡改

一种更微妙的原语:攻击者如果对现有 Session 具有 controller 级别的访问权限(在 Session 的 GUID 上拥有 TRACELOG_GUID_ENABLE 权限,通常需要 SeSystemProfilePrivilege 或 Session 所有权),可以尝试使用更窄的 ENABLE_TRACE_PARAMETERS 过滤器(例如 EVENT_FILTER_TYPE_PIDEVENT_FILTER_TYPE_EXECUTABLE_NAME)重新启用 Provider。对于在交付时遵守过滤器的 Provider 和 Session 类型,消费该 Session 的 EDR 可以看到一个干净的流,而所有其他进程继续正常发射。这需要管理员权限或 SeSystemProfilePrivilege,但不需要内核写入。

检测比许多文章所暗示的更棘手。TraceProviderBinaryTracking 用于将 Provider 事件映射回镜像二进制文件;它不是一个通用的"列出所有活动过滤器/控制器"API。可靠的生产模式是拥有 Session 句柄生命周期,记录 EDR 应用的确切 ENABLE_TRACE_PARAMETERS,订阅 Microsoft-Windows-Kernel-EventTracing 以获取启用/禁用元数据,定期在公共 API 暴露的地方重新查询 Provider/Session 状态,并将对受保护 Session/Provider 的任何外部 EnableTraceEx2 视为篡改。内核侧验证器还可以将 ETW_GUID_ENTRY.EnableInfo[i]FilterData 与其预期基线进行比较,但这本质上是对偏移敏感的。

11.9 Consumer 和交付路径篡改

Provider DKOM 并不是使依赖 ETW 的产品失明的唯一方法。实时 EDR 具有两个独立的信任边缘:Provider 到 Session 和 Session 到 Consumer。第一个是 ETW_GUID_ENTRY.EnableInfo[];第二个是 WMI_LOGGER_CONTEXT.Consumers 加上实时缓冲区交接状态。具有内核写入权限的攻击者可以保持 Provider 启用状态不变,同时使 Consumer 停止接收缓冲区:

Provider 仍处于启用状态
    EnableInfo[i].IsEnabled == 1
    RegEntry.EnableMask 位已设置

Session 不再有用
    WMI_LOGGER_CONTEXT.AcceptNewEvents = 0
    WMI_LOGGER_CONTEXT.LoggerStatus 已更改
    WMI_LOGGER_CONTEXT.Consumers 已取消链接或被毒化
    ETW_REALTIME_CONSUMER.DataAvailableEvent 从未被触发
    RealTimeBuffersLost / EventsLost 迅速攀升

这在操作上很有吸引力,因为仅询问"我的 Provider 是否已启用"的 controller 侧检查仍然通过。文件模式 Session 对 Consumer 取消链接更具弹性,因为刷新器将 ETL 缓冲区写入磁盘,但它们仍然可以通过设置 Session 状态、耗尽缓冲区、破坏刷新队列或通过病态事件率强制丢失来被饥饿。实时 Session 更容易被致盲,因为交付取决于 Consumer 对象及其通知路径保持健康。

因此,防御性检查必须验证整个路径:

  1. Provider 状态: 预期的 ETW_GUID_ENTRY.EnableInfo[i]TRACE_ENABLE_INFOETW_REG_ENTRY.EnableMask 和过滤器数据。
  2. Session 状态: AcceptNewEventsLoggerStatusEventsLostLogBuffersLostRealTimeBuffersLostNumberOfBuffersFreeBuffersConsumers
  3. Consumer 状态: WMI_LOGGER_CONTEXT.Consumers 上的预期 PID / 进程对象、活动进程句柄、回调活跃性以及应用层心跳。
  4. 端到端交付: 防御者拥有的金丝雀 Provider 发出周期性事件,用户态 Consumer 证明它在有界时间间隔内收到了确切的序列号。

金丝雀很重要,因为每个单独的内部字段都可能被伪造。端到端交付是唯一跨越所有三层的检查:Provider 写入、Session 接受、缓冲区刷新、Consumer 处理。

11.10 Hook 和绕过技术按 ETW 层映射

"Hooking and bypass" 部分中的参考文献通常被归为一组,但它们攻击的不是同一事物。有些使用 ETW 作为 Hook 交付机制,有些破坏 ETW 的 Provider 状态,有些仅绕过用户态 ETW 发射,有些对栈富集撒谎,有些根本不绕过 ETW。它们绕过了 EDR 对 ETW 的解释。

有用的阅读方式是按 ETW 层分类:

技术 / 系列 攻击的 ETW 层 它绕过了什么 它未绕过什么
经典 InfinityHook Session 热路径(WMI_LOGGER_CONTEXT.GetCpuClock 直接指针) 将 CKCL/NT Kernel Logger 定时路径用作系统调用 Hook 点 不是 ETW 致盲技术;在现代构建上直接指针字段已消失
现代 InfinityHook 系列 下游时间戳后端(KeQueryPerformanceCounter / HAL 定时器 QueryCounter 在 GetCpuClock 索引补丁后恢复 ETW 热路径控制 仍然不阻止 ETW 事件写入;如果防御者仅检查 GetCpuClock == 1,则误报风险高
ETW 上下文交换 Hook CKCL 上下文切换事件路径 / 时间戳后端 在线程转换时使用 ETW 始终在线的调度器跟踪路径进行内核控制 不阻止 ETW 事件写入;依赖于脆弱的调度器路径寄存器/栈布局
Wavestone / Binarly / EDRSandBlast 风格 ETW DKOM Provider 注册 / 启用状态 ETWTI、Kernel-Process 或供应商 Provider 发射 非 ETW 回调、minifilter、WFP、内存扫描
ntdll!Etw* 修补 / White Knight Labs 风格用户态修补 用户态生产者桩 该进程发出的 Manifest / TraceLogging 事件 内核 Provider、其他进程、AutoLogger 文件跟踪
无补丁硬件断点 + VEH 用户态调用过渡到 NtTraceEvent / AmsiScanBuffer 内联补丁检测和该线程上的用户态 Provider 发射 内核 ETW / ETWTI 用于实际进入内核的操作
LayeredSyscall 用户态 EDR Hook 和 VEH/调用栈检查层 基于 Hook 的系统调用监视器和朴素 VEH 遍历器 内核侧 ETWTI 负载
TamperingSyscalls 用户态系统调用参数影子 记录系统调用入口前"良性"参数的 EDR ETWTI 在参数验证后的内核侧事件负载
SilentMoonwalk, CallStackSpoofer, VulcanRaven ETW 栈富集 / 展开信任 使用 EVENT_HEADER_EXT_TYPE_STACK_TRACE64 的基于栈的归因规则 ETWTI 或 Kernel-Process 事件本身
RPC ETW 绕过 rpcrt4.dll 内 Provider 特定的生产者状态 被修补进程中 Microsoft-Windows-RPC 接口/调用遥测 内核网络、SMB/命名管道、Audit-API、进程/句柄遥测
EVENT_FILTER_TYPE_EXECUTABLE_NAME 过滤器反转 Controller/Session 过滤器状态 对选定进程名的受保护 Session 的交付 Provider 向其他 Session 的发射,如果审计完整则包括 controller/元 Provider

11.10.1 ETW 作为 Hook 面:InfinityHook 和上下文交换 Hook

InfinityHook 主要不是"从 ETW 隐藏"的技术。恰恰相反:它滥用了 ETW 无处不在的事实。在经典形式中,CKCL 的 WMI_LOGGER_CONTEXT.GetCpuClock 是一个可调用的函数指针。由于 CKCL 记录系统调用相关和调度器相关的事件,交换该指针使攻击者控制的内核例程在热路径上执行。被攻击的对象是 Session 控制块,而不是 Provider。

该原语很强大,因为它不需要修补 KiSystemCall64、SSDT 或 nt!Nt* 例程。攻击者骑乘一个合法的 ETW 时间戳回调:

系统调用入口
  -> PerfInfoLogSyscallEntry
      -> CKCL WMI_LOGGER_CONTEXT.GetCpuClock()
          -> 攻击者 Hook

现代 Windows 更改了承载字段。GetCpuClock 现在被视为一个小的时钟源选择器,而不是任意内核指针。这仅关闭了直接的 GetCpuClock = attacker_function 路径。后续系列向下或向侧移动一层:HAL 性能计数器函数指针交换针对选定时钟模式到达的时间戳后端,而上下文交换 Hook 针对发出 CSwitch 风格事件的 ETW/调度器路径。

检测逻辑不应询问"ETW 停止了吗?"。它应询问一个本应平淡无奇的 ETW 定时或调度器路径是否现在指向预期的内核/HAL 镜像区间之外。

11.10.2 Provider 状态 DKOM:Wavestone、Binarly、EDRSandBlast、FudModule

Wavestone/Binarly/EDRSandBlast/FudModule 系列攻击 Provider 启用和注册层。一旦攻击者具有内核写入权限,这是最干净的 ETW 致盲原语。

目标字段是使快速路径廉价的那些:

ETW_REG_ENTRY.EnableMask
ETW_REG_ENTRY.GroupEnableMask
ETW_GUID_ENTRY.ProviderEnableInfo.IsEnabled
ETW_GUID_ENTRY.EnableInfo[i].IsEnabled
ETW_GUID_ENTRY.EnableInfo[i].Level / MatchAnyKeyword / MatchAllKeyword
内核全局注册句柄,例如 EtwThreatIntProvRegHandle

绕过效果取决于更改了哪个字节。清除 Provider 的内核注册句柄使每个 EtwTiLog* 调用走"Provider 未注册"路径。清除 ProviderEnableInfo.IsEnabled 使 Provider 看起来全局禁用。清除一个 EnableInfo[i] 槽位仅致盲一个 Logger。将 MatchAnyKeyword 重写为不可能的关键字会饿死一个 Session 而没有明显禁用 Provider。这就是为什么这些技术很危险:它们不停止 EDR 服务,不一定停止 ETW Session,并且不需要修补可执行代码。

防御性答案是第 12.2 节中的完整性扫描加上金丝雀事件。

11.10.3 用户态生产者修补:ntdll!Etw*

经典的 White Knight Labs 风格用户态 ETW 绕过攻击进程内部的生产者桩。修补 ntdll!EtwEventWritentdll!NtTraceEventEtwEventEnabled 会影响其写入路径在该进程中运行的 Provider。PowerShell、.NET 运行时、RPC 客户端/服务器代码、AMSI 面向的用户态组件以及许多 TraceLogging Provider 都属于此类。

边界是严格的:

已修补的 powershell.exe:
    Microsoft-Windows-PowerShell 4104 可能消失
    来自该进程的 Microsoft-Windows-DotNETRuntime 事件可能消失

内核:
    Microsoft-Windows-Kernel-Process 仍发出进程启动事件
    ETWTI 仍发出远程 VM 写入 / 保护 / 设置上下文事件
    AutoLogger 文件模式 Session 仍接收源自内核的事件

因此,此类绕过的是本地用户态事件发射,而不是作为系统的 ETW。

11.10.4 无补丁用户态抑制:硬件断点、NtContinue、VEH

无补丁 ETW/AMSI 绕过攻击与内联修补相同的生产者边界,但避免更改字节。通常的配方是硬件断点加向量异常处理程序:

Dr0 = ntdll!NtTraceEvent
Dr1 = amsi!AmsiScanBuffer
VEH 处理 EXCEPTION_SINGLE_STEP
VEH 重写 RIP/RAX/结果状态
执行恢复,就像调用成功一样

ETW 特定的目标是进入 NtTraceEvent 的过渡。如果线程从未实际进入系统调用,用户态 Provider 的事件永远不会交给内核 ETW 写入器。NtContinue 很重要,因为它恢复当前线程上下文而不经过 ETWTI 通过 PspSetContextThreadInternal 记录的相同跨线程 SetThreadContext 路径。

这绕过了内联 Hook 检测,因为 ntdllamsi.dll 的字节保持干净。它不会绕过实际内核操作的内核 ETW。

11.10.5 LayeredSyscall 和参数篡改:绕过 EDR 的故事,而非 ETW

LayeredSyscall 和 TamperingSyscalls 经常被描述为 ETW 绕过,但更精确的说法是它们绕过了围绕系统调用的 EDR 解释层。

对于 ETWTI,这种区别很重要:

用户态传感器看到:
    NtProtectVirtualMemory(Base=X, Protect=PAGE_READONLY)

内核接收:
    NtProtectVirtualMemory(Base=X, Protect=PAGE_EXECUTE_READWRITE)

ETWTI 记录:
    可执行内存转换,因为 MiProtectVirtualMemory 看到了真实值

正确的检测器将内核起源的 ETWTI 负载与任何用户态影子遥测进行比较。如果它们不一致,用户态故事是可疑的那个。

11.10.6 栈欺骗:攻击 ETW 富集

SilentMoonwalk、CallStackSpoofer 和 VulcanRaven 攻击许多 EDR 附加到 ETW 事件的栈富集。它们通常不抑制事件。相反,它们使事件变得不那么有用。

事件仍然存在。内存分配、线程上下文更改或句柄操作仍然到达 ETWTI。变化的是调用链归因。防御者应将栈数据视为高价值但非自认证:根据 VAD 和已加载模块验证帧地址、检查展开元数据所有权、在可用时使用 CET 影子栈审计、并在正常栈看起来过于干净时在可疑范围跟踪中使用 LBR/上下文寄存器捕获。

11.10.7 Provider 特定的生产者状态:RPC ETW 绕过

RPC ETW 绕过攻击 rpcrt4.dll 内部的 Provider 特定缓存,而不是全局 ETW 子系统。如果攻击者修补了 lsass.exespoolsv.exe 或其他 RPC 服务器中的该进程本地缓存,RPC Provider 将停止在该进程中产生事件。ETW Session 可能仍然健康,其他 Provider 可能仍然正常发射。

这使得绕过范围狭窄但在操作上有价值。防御性的交叉视图是:

Kernel-Network 看到 RPC 流量
Kernel-File 看到命名管道传输
Audit-API 看到进程/句柄访问
Service Control / DCOM / SMB 遥测看到横向移动上下文
Microsoft-Windows-RPC 对同一服务器和间隔保持静默

该缺失就是信号。

11.10.8 过滤器反转:攻击 Controller 契约

EVENT_FILTER_TYPE_EXECUTABLE_NAME 和相关过滤器是合法的 Controller 功能。Controller 可以要求 ETW 仅交付选定的 PID、可执行文件名、事件 ID、负载模式或栈遍历目标。过滤器反转滥用发生在攻击者具有 controller 级别权限,缩窄或排除了防御者关心的进程时。

危险的案例很微妙:

Provider 已启用:是
Session 正在运行:是
Consumer 存活:是
来自良性进程的事件:是
来自目标进程的事件:否

防御措施是将过滤器视为完整性基线的一部分。记录防御者使用的确切 ENABLE_TRACE_PARAMETERS,订阅 Microsoft-Windows-Kernel-EventTracing 以获取启用/禁用更改,并在内核验证器可用时验证私有的 ETW_GUID_ENTRY.EnableInfo[i].FilterData

12. 防御与检测

12.1 硬件后盾:PatchGuard、KDP、HVCI

  • PatchGuard(KPP) 定期采样内核 .text 和选定的内核数据结构。一些 ETW 相关状态在现代构建上已知受到保护,但确切覆盖范围有意未文档化且随时间变化。
  • KDP(内核数据保护),在 VBS/HVCI 开启时可用,允许驱动对其自己的 .data 调用 MmProtectDriverSection,在 SLAT 级别将页标记为只读。Hypervisor 强制执行只读性。
  • HVCI 限制内核执行未签名页,这(结合 WDAC 驱动阻止列表)是对抗 BYOVD 的主要对策。

复合系统(Secure Boot + ELAM + HVCI + KDP + 驱动阻止列表 + 通过 TPM 2.0 的度量启动)是当前高风险反作弊(Vanguard 2024+)所要求的配置,因为这是在消费者 Windows 上内核 ETW 状态具有实际意义的防篡改能力的唯一配置。

12.2 内核 ETW 完整性检查

希望检测 FudModule 类篡改的 EDR 或反作弊驱动每几秒遍历其已知良好的 Provider 注册句柄,并与预期状态进行比较:

VOID PeriodicEtwIntegrityCheck(void)
{
    PETW_REG_ENTRY etwti = *(PETW_REG_ENTRY*)EtwThreatIntProvRegHandle;
    if (!etwti) { Report(ETW_TI_HANDLE_NULLED); return; }
    if (!MmIsAddressValid(etwti) || !MmIsAddressValid(etwti->GuidEntry)) {
        Report(ETW_TI_INVALID_POINTER); return;
    }

    PETW_GUID_ENTRY g = etwti->GuidEntry;
    if (!g->ProviderEnableInfo.IsEnabled)
        Report(ETW_TI_DISABLED);

    BOOLEAN ours = FALSE;
    for (int i = 0; i < 8; i++) {
        if (g->EnableInfo[i].LoggerId == g_OurLoggerId &&
            g->EnableInfo[i].IsEnabled) {
            ours = TRUE;
            if (g->EnableInfo[i].Level < g_ExpectedLevel)
                Report(ETW_LEVEL_DOWNGRADED);
            break;
        }
    }
    if (!ours) Report(ETW_SESSION_DETACHED);

    PWMI_LOGGER_CONTEXT ctx = EtwpLoggerContext[g_OurLoggerId];
    if (!ctx || ctx == (PVOID)1) { Report(ETW_CONTEXT_FREED); return; }
    if (!ctx->AcceptNewEvents) Report(ETW_NOT_ACCEPTING);

    LONG lostDelta = ctx->EventsLost - g_LastEventsLost;
    if (lostDelta > THRESHOLD) Report(EVENTS_LOST_SPIKE);
    g_LastEventsLost = ctx->EventsLost;
}

12.3 零事件检测

完整性检查的对偶是行为信号。一个具有大量活动但在本应产生事件报告的 Provider 上产生零事件的进程,要么是预先被排除在外,要么是主动绕过 ETW。

typedef struct {
    ULONG Pid;
    LARGE_INTEGER FirstSeen;
    LONG  EtwEventCount;       // ETW 路径
    LONG  NetworkConnects;     // WFP
    LONG  FileOperations;      // minifilter
    LONG  RegistryWrites;      // CmRegister 回调
    LONG  ModuleLoads;         // PsSetLoadImageNotify
} PROC_BEHAVIOR;

BOOLEAN SuspiciousZeroEvent(PROC_BEHAVIOR* b)
{
    ULONG activity = b->FileOperations + 5*b->NetworkConnects
                   + b->RegistryWrites + 2*b->ModuleLoads;
    if (activity > 100 && b->EtwEventCount < 5) return TRUE;
    return FALSE;
}

该信号是高置信度的。误报面很小。大多数执行实际工作的进程会发出一些 ETW 事件;不发出事件的进程要么是微不足道的,要么是恶意的。

12.4 纵深防御——正确的分层

Layer 1 - 用户态 ETW (ntdll)            绕过成本低;粗粒度信号
Layer 2 - 内核 ETW (Microsoft-Windows-Kernel-*)  可通过 DKOM 绕过
Layer 3 - ETWTI                               仅能通过 BYOVD 或路径规避绕过
Layer 4 - Hypervisor / VBS (CET, KDP, HVCI)   仅能通过 Hypervisor 级别的工作绕过

12.5 生产环境反作弊模式

在公开文档化的内核反作弊和高风险 EDR 中反复出现三种具体模式:

  1. 双槽 ETWTI 订阅。 从独立的内核组件占用八个 EnableInfo 槽位中的两个,迫使攻击者枚举并清除每个槽位以静默信号。
  2. 紧循环完整性扫描。 周期性检查,从高优先级内核定时器(通常亚秒级)重新读取 EtwThreatIntProvRegHandleg->ProviderEnableInfo.IsEnabled、相关的 EnableInfo[i] 槽位和 WMI_LOGGER_CONTEXT.AcceptNewEvents
  3. Provider/组 GUID 冗余。 同时通过 Provider GUID 和该 Provider 所属的任何组 GUID 进行订阅。

13. 活动跟踪、过滤和 Provider 组

13.1 活动 ID

EventActivityIdControl 管理一个每线程 GUID。EventWriteTransfer 接受一个 ActivityId 和一个 RelatedActivityId。该组合编码了跨异步操作的父子图。

对于安全分析师而言,最有用的属性是当被插桩的组件传播 ID 时,ETW 可以跨线程、进程和 IRP 边界保留关联性。

在实践中三种失败模式很重要:

  1. 线程本地泄漏。 用户态活动状态是线程本地的。
  2. 异步不连续性。 线程池跳转、APC、IOCP 完成或 RPC 回调没有自动的 ETW 父级。
  3. 对抗性欺骗。 活动 ID 是 GUID,不是能力。

对于反作弊和 EDR 工作,活动 ID 在检测器已经识别出强事件后最为有用。

13.2 过滤器

EVENT_FILTER_DESCRIPTOR.Type 支持多种过滤器类型;安全相关的如下:

Type 含义
EVENT_FILTER_TYPE_PID (0x80000004) 最多 8 个 PID;Session 级别执行
EVENT_FILTER_TYPE_EXECUTABLE_NAME (0x80000008) 镜像名称模式
EVENT_FILTER_TYPE_EVENT_ID (0x80000200) 包含/排除事件 ID
EVENT_FILTER_TYPE_EVENT_NAME (0x80000400) TraceLogging 事件名称包含/排除
EVENT_FILTER_TYPE_STACKWALK (0x80001000) 仅对命名事件 ID 启用栈捕获
EVENT_FILTER_TYPE_STACKWALK_NAME (0x80002000) TraceLogging 事件名称栈捕获
EVENT_FILTER_TYPE_STACKWALK_LEVEL_KW (0x80004000) 按级别/关键字栈捕获
EVENT_FILTER_TYPE_PAYLOAD (0x80000100) Provider 协作内容过滤器
EVENT_FILTER_TYPE_CONTAINER (0x80008000) 容器/Silo ID 过滤器

13.3 Provider 组

Provider 可以通过 EtwSetInformation(EventProviderSetTraits) 将自己注册到一个组 GUID 中。众所周知的组 GUID 包括 EventSource 组 {8FE0B58E-CB14-4DBD-A93D-1B11E20A1A9F}(.NET EventSource)、.NET 运行时组 {D5C8460C-83BC-27D0-95D8-1E5B9C6C0AB1} 以及 OpenTelemetry 使用的 DiagnosticSource 组 {C861D0E2-A2C1-44C2-AA12-D9F08FA8C7DC}

13.4 启用回调

PENABLECALLBACK 签名是:

VOID NTAPI EnableCallback(
    LPCGUID                   SourceId,
    ULONG                     IsEnabled,
    UCHAR                     Level,
    ULONGLONG                 MatchAnyKeyword,
    ULONGLONG                 MatchAllKeyword,
    PEVENT_FILTER_DESCRIPTOR  FilterData,
    PVOID                     CallbackContext);

CAPTURE_STATE 是未被充分利用的第三个值。它要求 Provider 立即发出"当前状态快照"。

13.5 通知 Provider

通知 Provider(哈希表中的 EtwpNotificationGuidType)允许双向通信。ETW_NOTIFICATION_TYPE 枚举:

名称 用途
0x01 NoReply 单向通知
0x02 LegacyEnable MOF Provider 启用
0x03 Enable 现代 Provider 启用(= ETW Enable)
0x04 PrivateLogger 私有 Logger 通知
0x05 PerfLib 性能计数器库
0x06 Audio 音频设备通知
0x07 Session Session 状态通知
0x09 CredentialUI consent.exe 凭据 UI 转换
0x0A InProcSession Win8.1+ 进程内 Session 通知
0x0B FilteredPrivateLogger 过滤的私有 Logger(Win10 1703+)

13.6 TraceLogging——宏如何成为自描述事件

TraceLoggingWrite 通过预处理器宏实现,这些宏展开为静态初始化的元数据结构加上一个小型运行时桩。两种 TraceLogging 特定的数据描述符类型:

#define EVENT_DATA_DESCRIPTOR_TYPE_NONE              0
#define EVENT_DATA_DESCRIPTOR_TYPE_EVENT_METADATA    1
#define EVENT_DATA_DESCRIPTOR_TYPE_PROVIDER_METADATA 2

EtwpWriteUserEvent 看到一个 Reserved == 12 的描述符时,它将该数据内联到事件头的元数据区域中,而不是有效负载中。

13.7 运行时 Provider 发现(EnumerateTraceGuidsEx、TraceQueryInformation)

ULONG EnumerateTraceGuidsEx(
    TRACE_QUERY_INFO_CLASS InformationClass,  // TraceGuidQueryList, TraceGuidQueryInfo,
                                              // TraceGuidQueryProcess
    PVOID                  InBuffer,
    ULONG                  InBufferSize,
    PVOID                  OutBuffer,
    ULONG                  OutBufferSize,
    PULONG                 ReturnLength);

TraceGuidQueryInfo 回答了"哪个进程注册了这个 Provider GUID,以及在哪些 Session 上启用了"的问题,无需内核调试器。

TraceQueryInformation 报告 Session 侧状态:

ULONG TraceQueryInformation(
    TRACEHANDLE               SessionHandle,
    TRACE_QUERY_INFO_CLASS    InformationClass,
    PVOID                     TraceInformation,
    ULONG                     InformationLength,
    PULONG                    ReturnLength);

值得注意的类:TraceProfileSourceConfigInfoTraceSystemTraceEnableFlagsInfoTraceMaxLoggersQueryTraceLbrConfigurationInfoTraceStackCachingInfoTraceUnifiedStackCachingInfoTraceContextRegisterInfoTracePmcSessionInformationTracePeriodicCaptureStateInfoTraceProviderBinaryTracking

13.8 通过 ETW 的硬件性能计数器

模式 1:相关 PMC。当 Session 启用 EVENT_ENABLE_PROPERTY_PMC_COUNTERS 时,每个事件携带一个附加的 EVENT_EXTENDED_ITEM_PMC_COUNTERS 扩展项。

模式 2:PMC 溢出采样。可配置的计数器阈值触发 PMU 溢出中断,内核将其转换为合成 PerfInfoPMCSample ETW 事件。

13.9 上下文寄存器、LBR、IPT 和栈缓存

上下文寄存器跟踪通过 TraceSetInformation(..., TraceContextRegisterInfo, ...) 控制。

LBR 跟踪通过 TraceLbrConfigurationInfo 配置分支过滤器。

栈缓存由 TraceStackCachingInfoTraceUnifiedStackCachingInfo 控制。

14. WPP、Manifest 二进制格式(WEVT_TEMPLATE / CRIM)、Crimson 通道

14.1 WPP

Windows Software Trace Preprocessor。tracewpp.exe 预处理用 DoTraceMessage(FLAG, fmt, ...) 宏注释的 C 源文件。解码 WPP 事件需要匹配的 PDB 或 .tmf 文件。

14.2 Manifest 二进制格式——CRIM 和 WEVT_TEMPLATE

基于 Manifest 的 Provider 将其编译后的 Manifest 存储为 WEVT_TEMPLATE 资源。该格式非正式地称为 CRIM(Compiled Resource Instrumentation Manifest):

"CRIM"  Size  MajorVer  MinorVer  NumberOfProviders
  [GUID, ProviderDataOffset] xN
  ┌─ WEVT_PROVIDER(每个 Provider)
  │    "WEVT"  Size  MessageId  NumElements  Reserved
  │    [ElementOffset, Unknown] xNumElements
  └─ 每个元素是以下之一:
         "CHAN"  通道定义
         "LEVL"  自定义级别
         "TASK"  任务
         "OPCO"  Opcode
         "KEYW"  关键字(每个 16 字节;64 位位掩码)
         "EVNT"  事件定义(每个 48 字节;通过偏移量引用模板)
         "MAPS"  值/位图映射的容器
         "BMAP"  位图(用于标志枚举)
         "VMAP"  值映射(用于普通枚举)
         "TTBL"  模板表(各个模板的容器)
         "TEMP"  各个模板(BinXml + 属性描述符)
         "PRVA"  私有/未文档化

14.3 TraceLogging Dynamic——运行时自描述事件

TraceLogging Dynamic(头文件:TraceLoggingDynamic.h)在运行时暴露自描述机制。

#include <TraceLoggingDynamic.h>

tld::Provider provider("MyDynamic.Provider",
                        tld::Guid("12345678-1234-1234-1234-1234567890AB"));
provider.Register();

tld::EventBuilder<std::vector<BYTE>> eb;
eb.Reset("EventName", tld::EventDescriptor(/*level*/3, /*keyword*/0x1));
eb.AddField("PID",       processId, tld::InType::UInt32);
eb.AddField("ImagePath", imagePath, tld::InType::CountedUtf16String);
eb.Write(provider);

14.4 隐藏的 TraceLogging Provider 考古(25H2 / 2026)

Asuka Nakajima 在 Black Hat Asia 2026 上的研究:扫描 C:\Windows\*C:\Program Files\* 下的 64 位二进制文件,发现了 2,672 个 TraceLogging Provider 和 64,000+ 个唯一事件,而基于 Manifest 的 Provider 大约有 920+ 个。

安全相关的例子:

TraceLogging Provider 二进制文件 用途
AttackSurfaceMonitor({c4e507b1-7224-4737-bde0-ced9284e7073} ntoskrnl.exe BYOVD 和可疑驱动设备滥用
Microsoft.Windows.Kernel.SysEnv({a9fdf37b-d72d-4051-a3cd-d422103ce079} ntoskrnl.exe Secure Boot / bootkit 侦察和篡改
Microsoft.Windows.ShellExecute({382b5e24-181e-417f-a8d6-2155f749e724} windows.storage.dll ClickFix 风格和间接进程启动检测

14.5 Crimson 通道

Crimson 是 ETW 之上的 Vista 时代事件日志基础设施。Admin 和 Operational 通道事件自动路由到持久化的 .evtx 文件,而 Analytic 和 Debug 事件则不持久化。

14.6 V2 属性和系统级私有 Logger

EVENT_TRACE_PROPERTIES_V2Wnode.Flags |= WNODE_FLAG_VERSIONED_PROPERTIES)通过 FilterDescCount / FilterDesc 和一个 V2Options 标志字进行扩展。

14.7 NT Kernel Logger:EnableFlags 与 PERFINFO_GROUPMASK

NT Kernel Logger(槽位 0)接受两种激活接口。第一种是 32 位 EnableFlags 字段。第二种是 256 位 PERFINFO_GROUPMASK

#define PERF_MASK_INDEX  (0xe0000000)
#define PERF_MASK_GROUP  (~PERF_MASK_INDEX)
#define PERF_NUM_MASKS   8

typedef struct _PERFINFO_GROUPMASK {
    ULONG Masks[PERF_NUM_MASKS];
} PERFINFO_GROUPMASK;

14.8 系统 Provider(Windows 10 SDK 20348+)

系统 Provider 替代 / 范围
SystemConfigProviderGuid PERF_CONFIG_SYSTEM,硬件 / 启动配置
SystemCpuProviderGuid / SystemProfileProviderGuid CPU 统计和分析采样
SystemSchedulerProviderGuid PERF_CONTEXT_SWITCH、PERF_DISPATCHER、PERF_PRIORITY
SystemProcessProviderGuid PERF_PROCESS、PERF_THREAD、PERF_LOADER
SystemIoProviderGuid / SystemIoFilterProviderGuid 磁盘 / 文件 / 网络 / I/O 过滤器活动
SystemMemoryProviderGuid PERF_MEMORY、PERF_VIRTUAL_ALLOC、PERF_HARD_FAULTS
SystemRegistryProviderGuid 注册表操作
SystemAlpcProviderGuid ALPC 活动
SystemPowerProviderGuid 电源转换、空闲状态
SystemInterruptProviderGuid PERF_INTERRUPT、PERF_DPC
SystemTimerProviderGuid 定时器活动
SystemHypervisorProviderGuid PERF_HV_PROFILE
SystemObjectProviderGuid 句柄和对象操作
SystemLockProviderGuid 锁争用
SystemSyscallProviderGuid PERF_SYSTEMCALL

15. ETW 的 WinDbg 取证

15.1 !wmitrace 扩展

!wmitrace.strdump                              所有 Logger
!wmitrace.logdump "NT Kernel Logger"           详细显示一个 Logger
!wmitrace.loglive  NT Kernel Logger            实时缓冲区状态
!wmitrace.guiddump                             已注册的 Provider/Control GUID
!wmitrace.tmffile  C:\symbols\driver.tmf       WPP 解码上下文
!wmitrace.searchpath C:\symbols                WPP TMF 搜索路径
!wmitrace.start "TestSession" /guid #...        启动/停止(仅限实时内核调试器)
!wmitrace.stop  "TestSession"

15.2 DX 对象模型

// 所有活动(非哨兵)Logger
dx ((nt!_WMI_LOGGER_CONTEXT*(*)[0x50])
     (((nt!_ESERVERSILO_GLOBALS*)&nt!PspHostSiloGlobals)
        ->EtwSiloState->EtwpLoggerContext))
   ->Where(l => l != 1 && l != 0)

// 槽位 0(NT Kernel Logger)的 EventsLost
dx (((nt!_ESERVERSILO_GLOBALS*)&nt!PspHostSiloGlobals)
        ->EtwSiloState->EtwpLoggerContext)[0]->EventsLost

// 附加到 Logger N 的实时 Consumer
dx (((nt!_ESERVERSILO_GLOBALS*)&nt!PspHostSiloGlobals)
        ->EtwSiloState->EtwpLoggerContext)[N]->Consumers

15.3 内存转储取证和 DiagTrack 工件

最高价值的单个取证工件是 %ProgramData%\Microsoft\Diagnosis\ETLLogs\AutoLogger\AutoLogger-Diagtrack-Listener.etl

15.4 启动时 ETW

25H2/24H2 中的初始化序列分为两个阶段:

  • 阶段 0 - EtwpInitialize(0)

    • 为主机 Silo 分配 ETW_SILODRIVERSTATE
    • 注册约 15 个核心内核 Provider,包括 ETWTI
    • 如果配置了,启动 GlobalLogger
    • 初始化启动 ETW 句柄存储
  • 阶段 1 - EtwpInitialize(1)

    • 完成其余内核 Provider 注册
    • 启动 CKCL(槽位 1,始终开启)
    • 遍历 AutoLogger 注册表项
    • ETW 拓扑现在完全生效

16. 安全 Provider 目录

16.1 进程 / 线程 / 模块 / 审计

Provider GUID 重要性原因
Microsoft-Windows-Kernel-Process {22fb2cd6-0e7b-422b-a0c7-2fad1fd0e716} 进程、线程、镜像事件 1/2/3/4/5/6
Microsoft-Windows-Kernel-Audit-API-Calls {e02a841c-75a3-4fa7-afc8-ae09cf9b7f23} NtOpenProcess、NtOpenThread、NtSetContextThread、NtTerminateProcess
Microsoft-Windows-Threat-Intelligence (ETWTI) {f4e1897c-bb5d-5668-f1d8-040f4d8dd344} 第 9 节——注入、LSASS 读取、劫持
Microsoft-Windows-Kernel-General {a68ca8b7-004f-d7b6-a698-07e2de0f1f5d} 时间变更、系统级事件
Microsoft-Windows-DotNETRuntime {e13c0d23-ccbc-4e12-931b-d9cc2eee27e4} 程序集加载、JIT、GC
Microsoft-Windows-DotNETRuntimeRundown {a669021c-c450-4609-a035-5af59af4df18} 订阅时的状态快照
Microsoft-Windows-RPC {6ad52b32-d609-4be9-ae07-ce8dae937e39} RPC 调用(接口 UUID + opnum)

16.2 内存、文件系统、注册表、网络

Provider GUID 用途
Microsoft-Windows-Kernel-Memory {d1d93ef7-e1f2-4f45-9943-03d245fe6c00} 内存摘要、工作集交换、ACG
Microsoft-Windows-Kernel-File {edd08927-9cc4-4e65-b970-c2560fb5c289} 文件创建/读取/写入/删除/重命名
Microsoft-Windows-Kernel-Registry {70eb4f03-c1de-4f73-a051-33d13d5413bd} 注册表创建/打开/设置/删除/查询
Microsoft-Windows-FilterManager {f3c5e28e-63f6-49c7-a204-e48a1bc4b09d} Minifilter 注册变更
Microsoft-Windows-NTFS {dd70bc80-ef44-421b-8ac3-cd31da613a4e} USN 日志、备用数据流
Microsoft-Windows-Kernel-Network {7dd42a49-5329-4832-8dfd-43d979153a88} TCP/UDP IPv4/IPv6 流事件
Microsoft-Windows-DNS-Client {1c95126e-7eea-49a9-a3fe-a378b03ddb4d} 通过事件 3008 进行 C2 检测
Microsoft-Windows-WFP {c22d1b14-c242-49de-9f17-1d76b8b9c458} 防火墙裁决
Microsoft-Windows-NDIS-PacketCapture {2ed6006e-4729-4609-b423-3ee7bcd678ef} 原始数据包——pktmon 的基础
Microsoft-Windows-WinHttp {7d44233d-3055-4b9c-ba64-0d47ca40a232} WinHTTP 流量

16.3 认证和脚本

Provider GUID 用途
Microsoft-Windows-Security-Auditing {54849625-5478-4994-a5ba-3e3b0328c30d} Security 通道事件 4624/4663/4688 的来源
Microsoft-Windows-LSA {cc85922f-db41-11d2-9244-006008269001} 认证包加载、SAM 访问
Microsoft-Windows-NTLM {ac43300d-5fcc-4800-8e99-1bd3f85f0320} NTLM 认证——Pass-the-Hash
Microsoft-Windows-Kerberos-KdcSvc {1bba8b19-7f31-43c0-9643-6e911f79a06b} TGT/TGS 发放
Microsoft-Windows-PowerShell {a0c1853b-5c40-4b15-8766-3cf1c58f985a} 事件 4104 ScriptBlock
PowerShellCore {f90714a8-5509-434a-bf6d-b1624c8a19a2} PS7 等价版本
Microsoft-Antimalware-Scan-Interface {2a576b87-09a7-520e-c21a-4942f0271d67} AMSI 扫描事件 1101

16.4 持久化、横向移动、自遥测

Provider GUID 用途
Microsoft-Windows-TaskScheduler {de7b24ea-73c8-4a09-985d-5bdadcfa9017} 计划任务变更
Microsoft-Windows-Services {0063715b-eeda-4007-9429-ad526f62696e} 服务安装/启动
Microsoft-Windows-WinRM {a7975c8f-ac13-49f1-87da-5a984a4ab417} PowerShell 远程处理
Microsoft-Windows-WMI-Activity {1418ef04-b0b4-4623-bf7e-d74ab47bbdaa} 事件 5859/5860/5861——永久 WMI 订阅
Microsoft-Windows-DistributedCOM {1b562e86-b7aa-4131-badc-b6f3a001407e} DCOM 激活
Microsoft-Windows-Sysmon {5770385f-c22a-43e0-bf4c-06f5698ffbd9} Sysmon 自身发出的事件
Microsoft-Windows-CodeIntegrity {4ee76bd8-3cf4-44a0-a0ac-3937643e37a3} HVCI / WDAC 决策

16.5 未充分文档化的 Provider

Provider GUID 用途
Microsoft-Windows-Kernel-EventTracing {b675ec37-bdb6-4648-bc92-f3fdc74d3ca2} 元 Provider——Session 启动/停止/启用事件
Microsoft-Windows-Kernel-Boot {15ca44ff-4d7a-4baa-bba5-0998955e531e} Bootkit / 早期驱动时间线
Microsoft-Windows-Kernel-PnP {9c205a39-1250-487d-abd7-e831c6290539} BYOVD 时间线
Microsoft-Windows-Hyper-V-Hypervisor {52fc89f8-995e-434c-a91e-199986449890} VM 进入/退出、EPT 违反
Microsoft-Windows-Schannel-Events {91cc1150-71aa-47e2-a7c8-4f2855bf4d24} TLS 握手
Microsoft-Windows-Security-Mitigations {fae10392-f0af-4ac0-b8ff-9f4d920c3cdf} CFG / EAF / IAF / ACG 强制事件
Microsoft-Windows-COM {d4263c98-310c-4d97-ba39-b55354f08584} DCOM 横向移动源

仅 TraceLogging 的 Provider:

TraceLogging Provider GUID / 来源 用途
AttackSurfaceMonitor {c4e507b1-7224-4737-bde0-ced9284e7073} / ntoskrnl.exe BYOVD 检测
Microsoft.Windows.Kernel.SysEnv {a9fdf37b-d72d-4051-a3cd-d422103ce079} / ntoskrnl.exe Secure Boot 侦察
Microsoft.Windows.ShellExecute {382b5e24-181e-417f-a8d6-2155f749e724} / windows.storage.dll ClickFix、间接启动链

16.6 事件 ID / 关键字参考(顶级 Provider)

[保留 Provider 指南格式、事件 ID、关键字等。]

16.7 .NET CLR ETW(Microsoft-Windows-DotNETRuntime)

Provider GUID:{e13c0d23-ccbc-4e12-931b-d9cc2eee27e4}。安全关键事件 ID:

ID 事件 重要性原因
141 MethodLoad JIT 编译的方法
143 MethodLoadVerbose 包含方法名和签名
145 MethodJittingStarted 每次反射加载时触发
152 ModuleLoad ModuleFlags & 0x4 = Dynamic = 内存中模块
154 AssemblyLoad AssemblyFlags & 0x2 = 动态程序集
80 ExceptionThrown_V1 对绕过噪声检测有用

16.8 商业 EDR 架构——工作示例

Microsoft Defender for Endpoint (MDE): MsMpEng.exe (PPL-AM)、MsSense.exe、MsSecFlt.sys。

CrowdStrike Falcon: CSAgent.sys 内核驱动、CSFalconService.exe。

Elastic Defend: 订阅 ETWTI,对每个 ETWTI 事件进行用户态调用栈捕获。

SentinelOne: 主要基于内核回调驱动;根据 2023+ 的公开报告,描述了一个 ELAM/受保护服务路径。

16.9 Sysmon——ETW 发射和回调冗余的工作示例

Sysmon 不是纯粹的 ETW Consumer。它拥有自己的内核驱动、回调集、过滤器以及自己的 ETW/EventLog Provider。

17. 检测架构

17.1 总体结构

                         服务端关联器
                               |
                  证据上传 / 策略决策
                               |
+------------------------------+------------------------------+
|                        用户态服务                             |
+------------------------------+------------------------------+
                               |
                          IOCTL 边界
                               |
+------------------------------+------------------------------+
|                       内核驱动                                |
+------------------------------+------------------------------+
                               |
                     Windows 内核 / ETW 结构

17.2 收集器优先级

[包含 11 个收集器的优先级表]

17.3 源信任表

[包含 11 个源的信住表]

17.4 Session 布局

AC_Process    Kernel-Process + ETWTI(Process/Thread/Image 关键字)
              实时,BufferSize 256-1024 KB,FlushTimer 1 秒
AC_Memory     ETWTI(仅 Memory 关键字)
              实时,BufferSize 512-1024 KB,FlushTimer 1 秒
AC_Thread     Kernel-Process(Thread 关键字)+ ETWTI(Thread 关键字)
              实时,BufferSize 512-2048 KB,FlushTimer 2 秒
AC_Backup     其他所有内容
              文件后端,BufferSize 1024-4096 KB,FlushTimer 30 秒,如支持则压缩

17.5 完整性验证

[检查族表——Producer、Session、交付、Controller 账本]

17.6 自适应取证

17.7 信号融合矩阵

[行为 vs ETW 源 vs 回调源表]

17.8 性能预算

+ 仅内核回调                     −2%
+ ETW(分离 Session,无栈)       −5%
+ ETW 完整性检查(5 秒周期)      −5.5%
+ 带栈遍历的 ETW                 −10%
+ 定期内存扫描                   −14%

17.9 缓冲区大小调整和丢失计数器监控

17.10 推荐顺序

17.11 验证剧本

[场景表——用户态修补、ETW 热路径 Hook、Provider DKOM、过滤反转、Consumer 饥饿、缓冲区洪泛、栈欺骗、隐藏 TraceLogging、启动间隙]

18. 精选漏洞

  • CVE-2023-21536——ETW 过滤器对象中的释放后使用
  • Notification reply 信息泄露(Winsider 2023)——EtwSendNotification 回复缓冲区泄漏内核内存
  • Provider 回调重入——未加锁的 Enable 回调与未加锁的 Disable 回调配对
  • WMI_LOGGER_CONTEXT 损坏导致的 Bug-check 0x139(KERNEL_SECURITY_CHECK_FAILURE)
  • ProcessTrace / 直接解析器无限循环(恶意 ETL 文件)
  • ETWHash(Ceri Coburn / TrustedSec,2022)——通过 Kerberos ETW Provider 进行凭据捕获
  • Microsoft-Windows-Kernel-EventTracing 盲点——元 Provider 在被篡改时不发出事件
  • Sealighter / 重新发射信任链——传递性信任边界

19. 行业背景(2024-2026)

三个转变塑造了这个时期:

  1. 内核 ETW 成为防御制高点。 用户态 ETW 的可靠性大致等同于其 ntdll 可能已被修补的进程。内核 ETW 的可靠性大致等同于路由它的内核数据结构和 Session 状态。
  2. Windows 弹性倡议改变了激励机制。 2024 年 7 月 CrowdStrike 宕机事件加速了 Microsoft 公开推动将更多端点安全能力迁移到更安全的平台提供遥测中。
  3. TraceLogging 将 Provider 目录变成了逆向工程问题。 2026 年的隐藏 TraceLogging 研究表明,仅靠 Manifest 视图看待 ETW 已不再足够。

剩下的战斗是无补丁绕过与 ETWTI 之间的较量。防御性答案是一个系统:KDP 保护的防御者状态、亚秒级完整性扫描、金丝雀事件心跳、双槽 ETWTI 订阅、Microsoft-Windows-Kernel-EventTracing 自监视、零事件行为关联,以及 CET 影子栈事件。

参考文献

Microsoft 官方文档

内核结构

安全研究

Hooking 和绕过

系统 Provider 和 PERFINFO_GROUPMASK

取证

Manifest 二进制格式

工具

商业 EDR 分析

凭据捕获 / 专业化

.NET CLR ETW

运行时发现 API

压缩和 PERFINFO_GROUPMASK

AMSI / PowerShell

漏洞案例研究

posted @ 2026-06-21 17:04  DirWangK  阅读(51)  评论(0)    收藏  举报