文章总结: 文章披露WindowsETW未公开SecurityTrace标志位可被管理员绕过,无需Antimalware-PPL即可停止受保护跟踪会话并消费Microsoft-Windows-Threat-Intelligence高权事件;作者通过hookControlTrace查询、利用AutoLogger或设置LogBuffersLost=0x4000启用标志,给出POC实现低权进程实时订阅威胁遥测,MSRC认为非漏洞但建议将校验移入内核。
综合评分: 85
文章分类: 威胁情报,漏洞分析,安全工具,二进制安全,AI安全
权限核查:ETW 的 SecurityTrace 标志奇案——无需 Antimalware-PPL 消费 Microsoft-Windows-Threat-Intelligence
CONNOR MCGARR
CONNOR MCGARR
securitainment
2026年1月21日 10:24
中国香港
| 原文链接 | 作者 |
| — | — |
| https://www.originhq.com/blog/securitytrace-etw-ppl | CONNOR MCGARR |
引言
最近,我们在为 Origin (by Prelude) Runtime Memory Protection research preview 产品调研新功能开发时,不得不深入挖掘 Event Tracing for Windows (ETW) 的内部机制。我们使用的内部 ETW 工具链以 Antimalware Protected Process Light (PPL) 的签名与保护级别运行。在此过程中,我们注意到一件反常的事:对一个启用了未公开的“security trace”标志的目标 ETW 会话,我们似乎可以在并不具备所需权限的情况下发出“stop trace”指令 (这正是本文主题)。该未公开标志似乎用于确保:只有以 Antimalware-PPL 运行的进程,才能与任何启用该标志的 ETW 跟踪会话交互或修改它。实践中,它似乎最适用于 AutoLogger ETW 跟踪会话 (后文会展示原因)。然而,我们却能仅凭管理员权限、无需任何特殊签名或更高的保护级别就停止该跟踪会话。熟悉 Windows 内核机制的读者会知道:即便这种边界并未被 官方明确承认,由受保护进程创建或管理的资源通常也不应被更低权限实体 (包括管理员进程) 修改。鉴于该标志似乎把跟踪会话的管理权完全委托给 Antimalware-PPL 进程,我们的兴趣被彻底点燃。
为了弄清这一切为何可能,我们最终不仅找到了无需 Antimalware-PPL 即可配置与管理这一未公开的“security trace”ETW 标志的方法。更重要且更具实用价值的是,我们据此识别出一种新的方式:在不以 Antimalware-PPL 运行、也不依赖内核驱动 (更不需要研究人员过去常用的各种“patch-the-kernel”技巧) 前提下,消费那些要求 Antimalware-PPL 的 ETW provider (提供程序) (例如 Microsoft-Windows-Threat-Intelligence) 所产生的事件。与本文配套,我们也发布了一个公开的概念验证 (POC):ThreatIntelligenceConsumer。
ETW 会话管理
尽管在 Windows 的用户态中暴露了创建与管理 ETW 会话的能力,但与跟踪会话相关的资源的 真正管理者仍然是内核。内核用来管理特定 ETW 会话的关键结构之一是 WMI_LOGGER_CONTEXT。
lkd> dt nt!_WMI_LOGGER_CONTEXT
+0x000 LoggerId : Uint4B
+0x004 BufferSize : Uint4B
+0x008 MaximumEventSize : Uint4B
+0x00c LoggerMode : Uint4B
+0x010 AcceptNewEvents : Int4B
+0x018 GetCpuClock : Uint8B
+0x020 LoggerThread : Ptr64_ETHREAD
+0x028 LoggerStatus : Int4B
+0x02c FailureReason : Uint4B
+0x030 BufferQueue : _ETW_BUFFER_QUEUE
+0x040 OverflowQueue :_ETW_BUFFER_QUEUE
+0x050 GlobalList : _LIST_ENTRY
+0x060 DebugIdTrackingList : _LIST_ENTRY
+0x070 DecodeControlList : Ptr64 _ETW_DECODE_CONTROL_ENTRY
+0x078 DecodeControlCount : Uint4B
+0x080 BatchedBufferList : Ptr64 _WMI_BUFFER_HEADER
+0x080 CurrentBuffer :_EX_FAST_REF
+0x088 LoggerName : _UNICODE_STRING
+0x098 LogFileName : _UNICODE_STRING
+0x0a8 LogFilePattern : _UNICODE_STRING
+0x0b8 NewLogFileName : _UNICODE_STRING
<--- Truncated --->
该结构体规模很大,承载了会话所需的大量数据与元数据:包括 logger 名称、正在写入的 ETW buffer 状态、Last Branch Record (LBR) 与 Intel Processor Trace (IPT) 的 ETW 启用状态 (若适用)、各类标志位 (flag) 以及其他值得关注的字段。本文中,我们重点关注其中的一组标志位。
+0x330 Flags : Uint4B
+0x330 Persistent : Pos 0, 1 Bit
+0x330 AutoLogger : Pos 1, 1 Bit
+0x330 FsReady : Pos 2, 1 Bit
+0x330 RealTime : Pos 3, 1 Bit
+0x330 Wow : Pos 4, 1 Bit
+0x330 KernelTrace : Pos 5, 1 Bit
+0x330 NoMoreEnable : Pos 6, 1 Bit
+0x330 StackTracing : Pos 7, 1 Bit
+0x330 ErrorLogged : Pos 8, 1 Bit
+0x330 RealtimeLoggerContextFreed : Pos 9, 1 Bit
+0x330 PebsTracing : Pos 10, 1 Bit
+0x330 PmcCounters : Pos 11, 1 Bit
+0x330 PageAlignBuffers : Pos 12, 1 Bit
+0x330 StackLookasideListAllocated : Pos 13, 1 Bit
+0x330 SecurityTrace : Pos 14, 1 Bit
+0x330 LastBranchTracing : Pos 15, 1 Bit
+0x330 SystemLoggerIndex : Pos 16, 8 Bits
+0x330 StackCaching : Pos 24, 1 Bit
+0x330 ProviderTracking : Pos 25, 1 Bit
+0x330 ProcessorTrace : Pos 26, 1 Bit
+0x330 QpcDeltaTracking : Pos 27, 1 Bit
+0x330 MarkerBufferSaved : Pos 28, 1 Bit
+0x330 LargeMdlPages : Pos 29, 1 Bit
+0x330 ExcludeKernelStack : Pos 30, 1 Bit
尽管这些标志位的掩码在结构上由 WMI_LOGGER_CONTEXT.Flags表示,但符号信息给出了可用取值的便利拆解。可以看到,SecurityTrace是其中一个标志位,也是本文的核心。然而,仅从名字我们只能推断它与安全相关,并不能直接看出它的具体用途。
为了理解该标志位可能用来做什么,我们首先枚举了所有包含该标志位的跟踪会话。需要注意的是,活动 logger 的数量不会超过 0x50 (80),这也是当前系统支持的 logger 最大数量。我们研究中使用的 WinDbg,是用于 ETW 分析的强大 工具。
lkd> dx ((nt!_WMI_LOGGER_CONTEXT*(_)[0x50])(((nt!_ESERVERSILO_GLOBALS_)&nt!PspHostSiloGlobals)->EtwSiloState->EtwpLoggerContext))->Where(l => l != 1).Where(l => l->SecurityTrace == 1).Select(i => i->LoggerName)
((nt!_WMI_LOGGER_CONTEXT*(_)[0x50])(((nt!_ESERVERSILO_GLOBALS_)&nt!PspHostSiloGlobals)->EtwSiloState->EtwpLoggerContext))->Where(l => l != 1).Where(l => l->SecurityTrace == 1).Select(i => i->LoggerName)
[5] : "DefenderApiLogger" [Type: _UNICODE_STRING]
[6] : "DefenderAuditLogger" [Type: _UNICODE_STRING]
启用该特性的跟踪会话只有两个:DefenderApiLogger 与 DefenderAuditLogger。它们与 Microsoft Defender 有关。如果你去分析相关二进制文件,并不会发现这些 ETW 会话的创建逻辑 (例如通过 StartTrace)。原因在于它们是以 AutoLogger ETW 会话的形式注册的。对不熟悉的读者而言,AutoLogger 会话用于让某些 logger 在启动过程中尽可能早地开始消费事件;它们不是由某个特定进程创建,而是由内核直接创建 (不同于大多数“普通”会话:通常由某个进程调用 StartTrace创建)。这些会话通过注册表 HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\WMI\Autologger进行配置。
AutoLogger 跟踪会话
通过观察 Microsoft Defender 相关的某个 AutoLogger 会话,我们或许能进一步理解 SecurityTrace特性。
DefenderApiLogger AutoLogger 跟踪会话
DefenderApiLogger 的注册表项本身包含符合 AutoLogger 规范的配置项 (并非所有 ETW 跟踪会话配置项都适用于 AutoLogger)。从上图可以看到,这里没有任何与“Flags”或类似命名相关的配置项,足以让人推断能配置 SecurityTrace等功能。更进一步,即便抛开 AutoLogger 会话,去看用于 StartTrace调用方以编程方式创建新 ETW 会话的 EVENT_TRACE_PROPERTIES结构体,里面 依然没有能够配置类似“security trace”特性或 SecurityTrace标志位的字段。
以 DefenderApiLogger 为例,我们能看到的只是:一组符合 AutoLogger 规范的 ETW 设置,以及一组该会话希望消费的各类 ETW provider 的 GUID 列表。例如,Microsoft-Windows-Services 这一 ETW provider 在子键中由第一个 GUID 0063715B-EEDA-4007-9429-AD526F62696E表示。每个这样的键还包含额外设置:例如某个 provider 应如何发出事件、logger 应如何消费事件;如果存在名为“Filters”的附加子键,则用来指定各类 ETW filter (如 event ID 过滤等)。
即便如此,这些配置里依然没有任何明显线索表明可以配置 SecurityTrace。鉴于 WMI_LOGGER_CONTEXT是内核专用结构体,我们继续向下追踪:内核态的 ETW runtime 是如何管理这一未公开的 SecurityTrace特性的。
SecurityTraceLogger 标志
我们已经多次提到 SecurityTrace标志位 (flag),但它到底 _做了什么_?最直接的方法之一是观察它在哪里被判断 (或被设置)。SecurityTrace被判断的主要位置在 Windows 内核中的 EtwpQueryTrace。顾名思义,该函数处理针对目标 ETW 会话的查询。在实际使用中,如果你用 logman这类内置工具获取某个 ETW 跟踪会话的细节,请求最终会流向 EtwpQueryTrace来完成处理。
EtwpQueryTrace
在解释 SecurityTrace如何在该函数中被判断之前,有必要先说明查询操作 _实际如何运作_,因为这会在本文后半部分变得非常关键。
ETW 会话的查询能力围绕 WMI_LOGGER_INFORMATION结构体展开。该结构体未公开,但它才是底层用户态调用方 NtTraceControl(通过 ControlTrace) 在多数 ETW 操作 (例如启动或查询一个 trace) 中真正使用的数据结构。也就是说,发送到内核的是它,而不是 Windows SDK 中那个更高层、且有文档描述的 EVENT_TRACE_PROPERTIES。虽然 Windows Research Kernel (WRK) 提供了该结构体的一个定义,但自 WRK 最后一次更新以来,该结构体已经发生了不少变化。幸运的是,在 combase.dll中仍能找到它 (顺带一提:COM 众所周知难以调试,因此 Microsoft 实际上为 combase.dll提供了私有符号。考虑到 COM 与 OS 的大量部分交织,这类信息往往可以从中挖掘出来)。
0:000> dt combase!_WMI_LOGGER_INFORMATION
+0x000 Wnode :_WNODE_HEADER
+0x030 BufferSize : Uint4B
+0x034 MinimumBuffers : Uint4B
+0x038 MaximumBuffers : Uint4B
+0x03c MaximumFileSize : Uint4B
+0x040 LogFileMode : Uint4B
+0x044 FlushTimer : Uint4B
+0x048 EnableFlags : Uint4B
+0x04c AgeLimit : Int4B
+0x04c FlushThreshold : Int4B
+0x050 Wow : Pos 0, 1 Bit
+0x050 QpcDeltaTracking : Pos 1, 1 Bit
+0x050 LargeMdlPages : Pos 2, 1 Bit
+0x050 ExcludeKernelStack : Pos 3, 1 Bit
+0x050 V2Options : Uint8B
+0x058 LogFileHandle : Ptr64 Void
+0x058 LogFileHandle64 : Uint8B
+0x060 NumberOfBuffers : Uint4B
+0x060 InstanceCount : Uint4B
+0x064 FreeBuffers : Uint4B
+0x064 InstanceId : Uint4B
+0x068 EventsLost : Uint4B
+0x068 NumberOfProcessors : Uint4B
+0x06c BuffersWritten : Uint4B
+0x070 LogBuffersLost : Uint4B
+0x070 Flags : Uint4B
+0x074 RealTimeBuffersLost : Uint4B
+0x078 LoggerThreadId : Ptr64 Void
+0x078 LoggerThreadId64 : Uint8B
+0x080 LogFileName :_UNICODE_STRING
+0x080 LogFileName64 :_STRING64
+0x090 LoggerName : _UNICODE_STRING
+0x090 LoggerName64 : _STRING64
+0x0a0 RealTimeConsumerCount : Uint4B
+0x0a4 SequenceNumber : Uint4B
+0x0a8 LoggerExtension : Ptr64 Voidf
+0x0a8 LoggerExtension64 : Uint8B
WMI_LOGGER_INFORMATION充当一个 翻译层:它把目标操作 (如 StartTrace或 ControlTrace) 所携带的 EVENT_TRACE_PROPERTIES中的信息,转换为对目标会话 WMI_LOGGER_CONTEXT的读写。
Sechost.dll是接收用户态高层查询请求的用户态组件。它会把EVENT_TRACE_PROPERTIES结构体转换成对应的WMI_LOGGER_INFORMATION结构体,随后将其发送到内核态,并由目标WMI_LOGGER_CONTEXT进行填充。之后,它再把结果翻译回调用方在ControlTrace查询操作中期望得到的EVENT_TRACE_PROPERTIES。这一翻译过程在Sechost.dll中通过EtwpCopyPropertiesToInfo(EVENT_TRACE_PROPERTIES->WMI_LOGGER_INFORMATION) 与EtwpCopyInfoToProperties(WMI_LOGGER_INFORMATION->EVENT_TRACE_PROPERTIES) 实现。
EtwpCopyPropertiesToInfo
这与 ETW 的“security trace”有何关系?正如我们在 EtwpQueryTrace中看到的那样,查询操作的实际功能会被目标会话的 WMI_LOGGER_CONTEXT.Flags.SecurityTrace这一位 (bit) 所门控。为了让目标会话的 WMI_LOGGER_INFORMATION从 WMI_LOGGER_CONTEXT中被填充 (换句话说:为了让 trace 查询操作真正发生),调用方进程 (即执行查询的进程,例如 logman.exe或任何其他 ControlTrace调用方) 必须至少具备 Antimalware-PPL 的签名级别 / 权限。
SecurityTracecheck
这意味着:即便一个进程具备例如 SYSTEM权限,也仍然无法查询这些 ETW 会话。
以 SYSTEM查询 SecurityTrace会话会失败
不过,SecurityTrace的用途不仅仅是限制用户态进程通过 ControlTrace+ EVENT_TRACE_CONTROL_QUERY来查询会话 (尽管这确实是它最核心的用途之一)。SecurityTrace标志位也会在其他与安全相关的路径中被检查。但有趣的是,当执行“停止 ETW 跟踪会话”的代码路径 (Sechost.dll中的 EtwpStopLogger、NT 中的 EtwpStopTrace) 时,却不存在这一检查。
在内核中,EtwpStopTrace调用的 EtwpStopLoggerInstance会检查 SecurityTrace位 (bit) 是否存在。但这项检查并 _不是_“安全相关”的 (即不会验证调用进程是否以 Antimalware-PPL 运行);它只是用于:当目标会话启用了 Microsoft-Windows-Security-Auditing provider 时,更新该 provider 的全局信息 (内核对其有特殊处理)。这是因为 (我们稍后会看到) 启用 SecurityTrace的一种方式,正是以一种非常特定的方式消费 Microsoft-Windows-Security-Auditing 这一 ETW provider。
Microsoft-Windows-Security-Auditing 相关处理
由于从发出 stop 指令到会话被停止的整个过程中,不存在明确的 Antimalware-PPL 检查 (并且 stop 操作也不会触发查询;而查询会隐式执行 Antimalware-PPL 检查),因此只要知道启用了 SecurityTrace的目标会话名称,一个仅具备管理员权限的进程 (对 Defender 会话而言,考虑到额外的安全描述符,实际往往需要 SYSTEM) 仍然可能停止带有 SecurityTrace标志位的 ETW 会话 (尽管查询该会话仍需要查询进程具备 Antimalware-PPL)。当然,如前所述,安全描述符等额外措施也可以进一步收紧对该类操作的权限要求。
eventProperties->Wnode.Guid = k_DefenderApiLoggerGuid;
eventProperties->LoggerNameOffset = sizeof(EVENT_TRACE_PROPERTIES);
error = ControlTraceW(0,
L"DefenderApiLogger",
eventProperties,
EVENT_TRACE_CONTROL_STOP);
if (error != ERROR_SUCCESS)
{
goto Exit;
}
wprintf(L"[+] Successfully stopped DefenderApiLogger trace session.\n");
在没有 Antimalware-PPL 的情况下停止 SecurityTrace跟踪会话
最后需要说明的是:SecurityTrace与 Antimalware-PPL 的检查几乎总是与内核函数 EtwCheckSecurityLoggerAccess成对出现。该函数才是实际执行权限验证 (请求 / 查询进程是否具备 Antimalware-PPL 权限以执行该操作) 的地方。它还 负责 确保:只有 Antimalware-PPL 进程能够为指定进程启用 Microsoft-Windows-Threat-Intelligence 相关遥测。即便启用了合适的 keyword,也并非所有 Microsoft-Windows-Threat-Intelligence 事件都会“默认”生成。例如,进程必须被 opt-in才会发出某些特定事件,如对内存的读 / 写。进程默认不会发出这些事件。
检查是否具备 Antimalware-PPL
总结一下:在我们看来,SecurityTrace标志位的目的似乎是防止非 Antimalware-PPL 进程访问启用该位 (bit) 的会话对应的 ETW 数据 (更具体地说:主要是 AutoLogger 跟踪会话,后文会展开)。这自然引出了两个问题:首先,如何才能启用该特性?其次,对启用了 SecurityTrace特性的会话而言,这会带来哪些潜在影响?
SecurityTrace– AutoLogger 会话
在分析中,我们识别出三种启用 SecurityTrace特性的方法。其中前两种方式,是通过对 AutoLogger 跟踪会话进行特定配置来间接触发的。
为了启用所有被请求的 provider,AutoLogger 会话在内核中会走一条特殊的代码路径 (这对后文非常关键)。AutoLogger 跟踪会话通过 EtwpEnableAutoLoggerProvider(而不是直接调用 EtwEnableTrace) 来启用其目标 provider。该函数首先会提取目标 AutoLogger 注册表项下的所有 provider 子键,按 provider GUID 进行遍历。如果目标 provider 中包含 Microsoft-Windows-Kernel-Audit-Api-Calls 或 Microsoft-Windows-Threat-Intelligence,目标 trace 的 WMI_LOGGER_CONTEXT就会被更新,使其包含 SecurityTrace标志位。
通过特定 ETW provider 启用 SecurityTrace
这里要记住的关键点是:这些会话并不是在某个特定进程的上下文中启动的——也就是说,此时不存在需要执行的 Antimalware-PPL 检查,因为发起请求的“进程”实际上是 System进程,换句话说就是内核本身。传统上,ETW 跟踪会话无法启用 Microsoft-Windows-Threat-Intelligence,因为当调用 EnableTraceEx2时会校验调用进程身份;若不是 Antimalware-PPL 进程,就会向调用方返回 access denied 错误。
AutoLogger 的差异在于:由于 AutoLogger 的 provider 启用过程不绑定到某个特定进程身份 (它并不涉及某个进程去调用 EnableTraceEx2),因此也就不存在针对 EnableTraceEx2调用方的身份检查。内核自身负责启用所有被请求的 provider (正如前面所示:它们列在每个 AutoLogger 的注册表项中)。这也解释了 SecurityTrace标志位的重要性:它的目的在于保护那些启用了高权限 provider (例如 Microsoft-Windows-Threat-Intelligence) 的 AutoLogger 会话,避免被非 Antimalware-PPL 进程消费。由于在启用时无法检查“启用者进程”的身份 (根本不存在可检查的进程上下文),OS 至少可以把检查延后到后续:当某个进程试图去 消费该会话时再进行验证。SecurityTrace正是在这里发挥作用。
第二种让 AutoLogger 启用该能力的方法,是设置一个未公开但有效的 AutoLogger 注册表配置值:EnableSecurityProvider。内核在 EtwpStartAutoLogger中实现这一逻辑 (注意:这里提到的 SecTraceUnion是 用户自定义的名字,并非我们前面提到的 WMI_LOGGER_INFORMATION结构体里真实使用的 union 名称。这里的 Flags与目标会话 WMI_LOGGER_CONTEXT中的 Flags是 1:1 映射关系,后文会更清楚)。
SecurityTraceenablement via EnableSecurityProvider
需要强调的是:当设置 EnableSecurityProvider这一 AutoLogger 键时,还会触发一些额外的隐式动作。任何设置了该键的 AutoLogger 都会 自动被 opt-in 去消费 Microsoft-Windows-Security-Auditing 这一 ETW provider;并且目标 logger ID 会通过内核中由 PspHostSiloGlobals管理的 ETW_SILODRIVERSTATE结构体,被加入到已知消费该 provider 的 logger 列表中。之所以如此,是因为在 EtwpPreInitializeSiloState中,EtwpSecurityProviderGuidEntry总是被设置为 Microsoft-Windows-Security-Auditing provider。
Microsoft-Windows-Security-Auditing 配置
此外,在 EtwpPreInitializeSiloState中,EtwpSecurityLoggers数组的第一个 logger ID 被硬编码为 3——它总是为 EventLog-Security 跟踪会话保留。并且如前所述,任何指定 EnableSecurityProvider注册表值的 AutoLogger,都会被加入该列表,同时启用 SecurityTrace位 (bit)。
EventLog-Security 配置
除此之外,还存在一种“非 AutoLogger”的方法:无需以 Antimalware-PPL 运行,即可启用 SecurityTrace标志位 (甚至无需依赖 AutoLogger 注册表键,就能动态 / 编程式完成)。此外,我们也会说明:如何在没有 Antimalware-PPL 的情况下消费这类 trace。
WMI_LOGGER_INFORMATION
如前所述,在用户态中,文档化的 EVENT_TRACE_PROPERTIES结构体与内核态的 WMI_LOGGER_CONTEXT结构体之间存在一层抽象:WMI_LOGGER_INFORMATION。观察该结构体时,我们能发现一些有趣的行为,尤其体现在 Flags与 LogBuffersLost上:
0:000> dt combase!_WMI_LOGGER_INFORMATION
<--- Truncated --->
+0x070 LogBuffersLost : Uint4B
+0x070 Flags : Uint4B
<--- Truncated --->
如上所示,这两个成员位于同一内存位置 (offset 0x70)。这暗示它们实际上属于同一个 union(我们先前用SecTraceUnion来表示),同一时刻只能有一个值有效。也就是说:文档化结构体EVENT_TRACE_PROPERTIES中的LogBuffersLost,与文档中不存在的另一个成员Flags形成 union。并且如前所述,该Flags成员会从用户态提供的中间结构WMI_LOGGER_INFORMATION中被直接导入到内核态WMI_LOGGER_CONTEXT结构体的Flags字段。
WMI_LOGGER_INFORMATIONunion
在我们的场景中,由于 StartTrace传入的EVENT_TRACE_PROPERTIES结构体包含LogBuffersLost,且它与Flags位于同一个 union 中,因此:如果在调用StartTrace时将LogBuffersLost设置为0x4000(这正是WMI_LOGGER_CONTEXT.Flags中SecurityTrace位 (bit) 的掩码),该值就会被直接导入到目标WMI_LOGGER_CONTEXT结构体中!其原因仍然是:Sechost.dll中的EtwpCopyPropertiesIntoInfo(EVENT_TRACE_PROPERTIES->WMI_LOGGER_INFORMATION) 会对该 union 数据做直接拷贝。
EtwpCopyPropertiesToInfoupdated WMI_LOGGER_INFORMATIONfrom EVENT_TRACE_PROPERTIES, including Flags
这就使得我们可以在不以 Antimalware-PPL 运行的情况下,以编程方式启用 SecurityTrace;甚至不需要依赖一个启用了需要 Antimalware-PPL 才能消费的 provider 的 AutoLogger 跟踪会话。需要注意的是:该标志位必须在调用 StartTrace时设置 (无法通过 ControlTrace传入一个更新后的 EVENT_TRACE_PROPERTIES来修改 LogBuffersLost;内核在 EVENT_TRACE_CONTROL_UPDATE的更新场景下会忽略该值)。
//
// <snip>
//
traceProperties->LogBuffersLost = 0x4000; // Treated as "Flags" if 0x4000 is set in nt!EtwpStartLogger.
error = StartTraceW(TraceHandle,
TraceName,
traceProperties);
if (error != ERROR_SUCCESS)
{
wprintf(L"[-] Error in StartTraceW! (Error: 0x%lx)\n", error);
goto Exit;
}
在调用 StartTrace时将 LogBuffersLost设为 0x4000之后,目标 trace 的 WMI_LOGGER_CONTEXT中就会设置 SecurityTracebit。
3: kd> dx ((nt!_WMI_LOGGER_CONTEXT*(_)[0x50])(((nt!_ESERVERSILO_GLOBALS_)&nt!PspHostSiloGlobals)->EtwSiloState->EtwpLoggerContext))->Where(l => l != 1).Where(l => l->SecurityTrace == 1).Select(i => i->LoggerName)
((nt!_WMI_LOGGER_CONTEXT*(_)[0x50])(((nt!_ESERVERSILO_GLOBALS_)&nt!PspHostSiloGlobals)->EtwSiloState->EtwpLoggerContext))->Where(l => l != 1).Where(l => l->SecurityTrace == 1).Select(i => i->LoggerName)
[5] : "DefenderApiLogger" [Type: _UNICODE_STRING]
[6] : "DefenderAuditLogger" [Type: _UNICODE_STRING]
[41] : "MyTrace" [Type: _UNICODE_STRING]
因此,我们 确实可以创建一个阻止任何非 Antimalware-PPL 进程查询的 trace 会话!这对某些软件尤其有用:它们希望创建一个不易被其他进程发现的受保护 ETW 会话 (并且不需要为此创建 AutoLogger 注册表键)。
问题在于:就目前而言,这样做看似毫无意义,因为当我们真正要从该 trace 会话消费 ETW 事件时仍会卡住。到目前为止我们已经看到:几乎所有启用了 SecurityTrace的场景都默认“消费该 trace 的目标进程”会以 Antimalware-PPL 运行 (尽管我们已经知道:一个 不以 Antimalware-PPL 运行的进程也能启用该特性)。
如果要用文档化 API 来消费事件,我们通常需要两个调用:OpenTrace与 ProcessTrace。对 real-time ETW consumer 来说,OpenTrace与 ProcessTrace内部会调用 Sechost.dll中的私有函数 EtwpQueryRealTimeTraceProperties。
EtwpQueryRealtimeTracePropertiesunion
该函数会在 OpenTrace与ProcessTrace的执行路径中以内联方式出现。这里的根本问题是:调用这两个函数会隐式调用ControlTrace并使用EVENT_TRACE_CONTROL_QUERY控制码,从而触发一次对内核的查询操作。正如前面所说,SecurityTrace必须在StartTrace调用时设置且无法更新,因此当执行到EtwpQueryRealTimeTraceProperties时,SecurityTrace位 (bit) 已经处于设置状态。由于查询操作会触发SecurityTrace位 (bit) 的检查 (而我们发起OpenTrace/ProcessTrace的进程并非Antimalware-PPL),因此操作会以ERROR_ACCESS_DENIED失败。这也解释了我们前面的结论:没有 Antimalware-PPL 就无法消费启用了SecurityTrace的 trace 会话。然而,鉴于该检查发生在用户态,事情并不止于表面看到的这样!
在没有 Antimalware-PPL 的情况下消费 SecurityTrace会话
消费失败的根本原因在于查询操作。不过,由于这一检查被委托给用户态 (而不是在内核中作为 NtTraceControl消费事件调用的一部分 内联执行),并且我们完全控制发起 OpenTrace与 ProcessTrace的进程,因此我们可以绕过该检查,从任何启用了 SecurityTrace的 trace 会话中消费数据。主要有两种选择:
- 只使用
ntdll.dll的 native API (主要是NtTraceControl) 来消费该 trace 会话。由于OpenTrace与ProcessTrace属于高层 API,直接调用 native API 可以绕过查询操作 - 在
EtwpQueryRealTimeTraceProperties(或ControlTrace本身) 上安装 hook,把所有查询操作重定向到我们自己的实现。可以使用 Microsoft Detours 等受支持的库,也可以自行实现 hook
出于时间限制,我们选择了第二种方案,并使用了自研的简单函数 hook (未使用 Detours 或其他库)。既然使用函数 hook,我们就需要补齐一些细节。首先,要向 EtwpQueryRealTimeTraceProperties的调用方返回其期望的全部信息,包括:
-
系统的处理器数量
-
HistoricalContext(它被称为“trace handle”,但本质上只是保存在
ETW_REALTIME_CONSUMER结构体中的 logger ID;或者说:该会话WMI_LOGGER_CONTEXT在内核PspHostSiloGlobals->EtwSiloState的EtwpLoggerContext数组中的位置) -
需要返回给调用方的“最终”
EVENT_TRACE_PROPERTIES(大小必须为 0x1078 字节) -
返回码
ERROR_SUCCESS(0)
不过,上述讨论对应的是在 EtwpQueryRealTimeTraceProperties上安装 hook 的情况。由于这是一个 私有函数 (Etwp前缀即可看出),它并未导出;为了保持 POC 的可移植性 / 可更新性,维护成本会更高。对 POC 来说更可移植的方法,是仅针对查询操作在 ControlTrace上安装 hook。ControlTrace是导出的,其地址总是可知。这样我们只需返回一个“success”错误码,并提供输出的 trace properties 即可。需要注意的是,用于获取处理器数量的方式之一 TraceQueryInformation,不会导致对内核 EtwpQueryTrace的 实际调用。
回到 EtwpQueryRealTimeTraceProperties:这次查询操作很可能是为了从内核获取目标 trace properties 的“可信副本”,并且顺带执行SecurityTrace位 (bit) 的检查。通过反复试验我们发现:只要提供最初StartTrace调用返回的EVENT_TRACE_PROPERTIES就足够了,不需要内核查询返回的那份 properties。因此,对我们而言,只需要把ControlTrace的查询操作 detour 到自己的 hook,并向调用方返回我们已经从StartTrace填好的 trace properties 即可!该ControlTracehook 只需识别目标操作是否为 query;若是,就把目标 trace properties 返还给EtwpQueryRealTimeTraceProperties(后者在正常执行过程中会填充HistoricalContext与处理器数量)。
ControlTracehook
上述代码在不执行实际查询操作的情况下,向 EtwpQueryRealTimeTraceProperties的调用方返回其所需信息 (如前所述,实际查询会因为消费进程不以 Antimalware-PPL 运行而失败)。仅通过插入这一 thunk,我们就能在没有 Antimalware-PPL 的情况下,成功消费设置了 SecurityTrace位 (bit) 的 trace 会话中的 ETW 事件!同样的方法也可用于在没有 Antimalware-PPL 的情况下消费受保护的 ETW provider,例如 Microsoft-Windows-Threat-Intelligence。
在没有 Antimalware-PPL 的情况下消费 Microsoft-Windows-Threat-Intelligence
如前所述,SecurityTrace位 (bit) 的核心目的,是在 AutoLogger 场景下保护那些希望消费高权限 ETW provider (例如 Microsoft-Windows-Threat-Intelligence) 的 ETW 跟踪会话。原因并不复杂:内核中为目标 trace 会话启用某个 ETW provider 的代码路径,取决于该 trace 会话是否为 AutoLogger 会话。如果 trace 会话不是 AutoLogger,那么在不具备 Antimalware-PPL 的情况下就无法消费 Microsoft-Windows-Threat-Intelligence provider。这源于内核态 EtwpCheckNotificationAccess中的检查 (回忆一下:AutoLogger 启用时没有可调用 EnableTraceEx2的“进程上下文”,因为所有 AutoLogger 会话由内核负责建立)。
EtwpCheckNotificationAccesscheck
问题在于:AutoLogger ETW 跟踪会话的实际检查逻辑是不同的。如果 Microsoft-Windows-Threat-Intelligence provider 是被 AutoLogger 消费的,那么只会检查 SecurityTrace标志位——并不会调用 EtwpCheckNotificationAccess,因为根本没有可供验证的进程上下文。这是因为所有 AutoLogger 会话由内核自身实例化,而不是由某个特定进程创建。我们之前也解释了 AutoLogger 如何首先设置 SecurityTrace位 (bit)。基于此,我们可以构建如下流程:
- 在 AutoLogger 注册表键中创建一个条目以消费 Microsoft-Windows-Threat-Intelligence。这会在该 trace 会话中启用 Microsoft-Windows-Threat-Intelligence。注意:此时 trace 尚未被某个目标进程消费,因此不会发生 Antimalware-PPL 检查;因为在该阶段它并不适用——创建这些会话的是内核,而不是某个特定进程
- 在用户态 patch
ControlTrace,使其允许从设置了SecurityTracebit 的 trace 中消费。我们只需提供目标EVENT_TRACE_PROPERTIES结构体 - 正常调用
OpenTrace与ProcessTrace。这样就能在 不执行我们先前展示的查询操作的情况下,完成消费所需的一切
上述实现中唯一的难点在于 EVENT_TRACE_PROPERTIES。在最初的 POC 中,我们之所以能简化问题,是因为在最初调用StartTrace后,我们就拥有一个被完整填充的EVENT_TRACE_PROPERTIES结构体。但现在我们要消费一个已存在的 AutoLogger 会话,无法再调用StartTrace(会话已经存在)。这意味着:我们必须手动填充一个EVENT_TRACE_PROPERTIES结构体,并把它返回给Sechost.dll中EtwpQueryRealTimeTraceProperties的调用方。回忆一下:由于设置了SecurityTrace,我们无法在没有 Antimalware-PPL 的情况下直接查询这些 properties。通过反复试验我们发现,要让调用成功 (以及让整个OpenTrace/ProcessTrace流程成功),EVENT_TRACE_PROPERTIES结构体中需要以下字段:
-
所有相关的
WNODE_HEADER字段 (Guid等),尤其是HistoricalContext -
BufferSize(一个有效值;此处我选择了
0x40) -
LogFileMode(
EVENT_TRACE_REAL_TIME_MODE) -
FlushTimer -
MinimumBuffers -
LoggerNameOffset
除 HistoricalContext外,上述字段都很容易填:只需要与注册表中目标 AutoLogger 跟踪会话的设置对齐即可。HistoricalContext也并非不可解,它是确定性的:如前所述,它其实就是 logger 的 ID。既然我们消费的是 AutoLogger 跟踪会话,那么与我们目标会话相关的 ID,必然来自 AutoLogger 注册表键在分配该会话 ID 时所包含的条目。此外,AutoLogger 会按字母顺序启用 (少数例外很容易补偿)。
经测试,似乎第一个被使用的 logger ID 总是 2 (用于“传统”kernel logger 会话);并且我们前面也知道 ID 3 总是为 EventLog-Security trace 保留——因此第一个可能的 ID 是 4。把这些规则纳入考虑后,我们就可以通过对 4 – 80 (最大 ID) 进行 query 操作的方式暴力枚举,从而推断目标会话的 HistoricalContext。AutoLogger 总是优先占用较小的 ID (从 4、5、6 等开始),因此在 4 – 80 的范围内迭代,直到发现某个值的查询返回 ERROR_ACCESS_DENIED,通常是一个很好的信号:目标 trace 会话很可能是 SecurityTrace目标 (尽管 并非总是如此,因为查询失败也可能由其他与 SecurityTrace无关的原因导致)。我们发布的是 POC,因此更完善的 trace ID 求解实现留给读者作为练习:毕竟 trace ID 本身只是数值,而 AutoLogger 也基本按字母顺序启用。在我们发布的 POC 中,我们简单地创建一个以 0 开头的 trace 会话名。对 POC 目的而言,这几乎可以保证该会话在大多数情况下会按字母顺序排在最前,从而获得注册 trace 会话中的第一个 ID (4)。
最后,在通过相关检查后,我们就能在没有 Antimalware-PPL、也不需要任何内核态内存 patch 或驱动加载的情况下,消费 Microsoft-Windows-Threat-Intelligence ETW provider。
void
HandleThreatIntelligenceCallback (
_In_ PEVENT_RECORD EventRecord
)
{
wprintf(L"[+] [HandleThreatIntelligenceCallback] Hello from the Threat-Intelligence ETW callback!\n");
//
// Print the GUID
//
wprintf(L" [*] GUID = {%08lX-%04hX-%04hX-%02hhX%02hhX-%02hhX%02hhX%02hhX%02hhX%02hhX%02hhX}\n",
EventRecord->EventHeader.ProviderId.Data1,
EventRecord->EventHeader.ProviderId.Data2,
EventRecord->EventHeader.ProviderId.Data3,
EventRecord->EventHeader.ProviderId.Data4[0],
EventRecord->EventHeader.ProviderId.Data4[1],
EventRecord->EventHeader.ProviderId.Data4[2],
EventRecord->EventHeader.ProviderId.Data4[3],
EventRecord->EventHeader.ProviderId.Data4[4],
EventRecord->EventHeader.ProviderId.Data4[5],
EventRecord->EventHeader.ProviderId.Data4[6],
EventRecord->EventHeader.ProviderId.Data4[7]);
return;
}
正在消费 Microsoft-Windows-Threat-Intelligence
可以看到,此处的 GUID 正是 Microsoft-Windows-Threat-Intelligence 的 GUID (F4E1897C-BB5D-5668-F1D8-040F4D8DD344)。此外,如果我们通过 WMI_LOGGER_CONTEXT中的链表枚举挂载到该 trace 会话的 consumer 列表 (ETW_REALTIME_CONSUMER结构体列表),就能看到:消费该会话、并启用了 Microsoft-Windows-Threat-Intelligence provider 的唯一进程,并不具备 Antimalware-PPL,它正是我们的 POC 进程!
3: kd> dx ((nt!_WMI_LOGGER_CONTEXT*(_)[0x50])(((nt!_ESERVERSILO_GLOBALS_)&nt!PspHostSiloGlobals)->EtwSiloState->EtwpLoggerContext))->Where(l => l != 1).Where(l => l->SecurityTrace == 1).Select(i => new { Name = i->LoggerName, Consumers = Debugger.Utility.Collections.FromListEntry(i->Consumers, "nt!_ETW_REALTIME_CONSUMER", "Links")})[0n4].Consumers[0]
((nt!_WMI_LOGGER_CONTEXT*(_)[0x50])(((nt!_ESERVERSILO_GLOBALS_)&nt!PspHostSiloGlobals)->EtwSiloState->EtwpLoggerContext))->Where(l => l != 1).Where(l => l->SecurityTrace == 1).Select(i => new { Name = i->LoggerName, Consumers = Debugger.Utility.Collections.FromListEntry(i->Consumers, "nt!_ETW_REALTIME_CONSUMER", "Links")})[0n4].Consumers[0] [Type: _ETW_REALTIME_CONSUMER]
[+0x000] Links [Type:_LIST_ENTRY]
[+0x010] ProcessHandle : 0xffffffff800037b0 [Type: void *]
[+0x018] ProcessObject : 0xffffa58900524080 [Type:_EPROCESS*]
[+0x020] NextNotDelivered : 0x0 [Type: void *]
[+0x028] RealtimeConnectContext : 0x0 [Type: void*]
[+0x030] DisconnectEvent : 0xffffa5890188e2e0 [Type: _KEVENT *]
[+0x038] DataAvailableEvent : 0xffffa5890188e760 [Type:_KEVENT*]
[+0x040] UserBufferCount : 0x202d0255450 : Unable to read memory at Address 0x202d0255450 [Type: unsigned long *]
[+0x048] UserBufferListHead : 0x202d0255448 [Type: _SINGLE_LIST_ENTRY*]
[+0x050] BuffersLost : 0x0 [Type: unsigned long]
[+0x054] EmptyBuffersCount : 0x0 [Type: unsigned long]
[+0x058] LoggerId : 0x4 [Type: unsigned short]
[+0x05a] Flags : 0x0 [Type: unsigned char]
[+0x05a ( 0: 0)] ShutDownRequested : 0x0 [Type: unsigned char]
[+0x05a ( 1: 1)] NewBuffersLost : 0x0 [Type: unsigned char]
[+0x05a ( 2: 2)] Disconnected : 0x0 [Type: unsigned char]
[+0x05a ( 3: 3)] Notified : 0x0 [Type: unsigned char]
[+0x05a ( 4: 4)] Wow : 0x0 [Type: unsigned char]
[+0x060] ReservedBufferSpaceBitMap [Type:_RTL_BITMAP]
[+0x070] ReservedBufferSpace : 0x202d0360000 : Unable to read memory at Address 0x202d0360000 [Type: unsigned char *]
[+0x078] ReservedBufferSpaceSize : 0x80000 [Type: unsigned long]
[+0x07c] UserPagesAllocated : 0x0 [Type: unsigned long]
[+0x080] UserPagesReused : 0x3d [Type: unsigned long]
[+0x088] EventsLostCount : 0x202d0255368 : Unable to read memory at Address 0x202d0255368 [Type: unsigned long*]
[+0x090] BuffersLostCount : 0x202d025536c : Unable to read memory at Address 0x202d025536c [Type: unsigned long *]
[+0x098] SiloState : 0xffffa588f8631000 [Type:_ETW_SILODRIVERSTATE*]
3: kd> dx ((nt!_EPROCESS*)0xffffa58900524080)->Protection
((nt!_EPROCESS*)0xffffa58900524080)->Protection [Type: _PS_PROTECTION]
[+0x000] Level : 0x0 [Type: unsigned char]
[+0x000 ( 2: 0)] Type : 0x0 [Type: unsigned char]
[+0x000 ( 3: 3)] Audit : 0x0 [Type: unsigned char]
[+0x000 ( 7: 4)] Signer : 0x0 [Type: unsigned char]
需要提醒读者的是:该 POC 并不能启用那些在进程上默认关闭的遥测来源。例如,要启用 impersonation events,仍然需要 Antimalware-PPL 来调用 NtSetInformationProcess;这些事件必须通过该特权系统调用显式启用,而本 POC 无法完成这一点。本文方法默认能够消费以下遥测 (即无需另行通过特权系统调用针对每个进程启用,就会自然发出的遥测):
- 可执行内存分配事件 (user-mode 与 kernel-mode 调用方)
- 可执行内存映射事件 (user-mode 与 kernel-mode 调用方)
- 远程 APC 事件 (user-mode)
- 线程上下文更新事件 (SetThreadContext)
- kernel-mode 设备与驱动加载 / 卸载事件
- 系统调用事件:在本文撰写时,仅包括 NtSystemDebugControl 与 NtQuerySystemInformation 系统调用
同时也值得指出:在最新的 Windows Insider Preview 版本 (Canary channel) 中,已经有一些进程被 opt-in 到“可选”遥测 (包括内存保护、进程/线程挂起以及其他事件)。这意味着:使用本文的方法将可以“免费”接收到这些事件。其原因在于 Microsoft Defender 会调用相关功能;由于它以 Antimalware-PPL 运行,因此能够为其他进程启用这些“可选”遥测位 (bit)。
已 opt-in 到可选 Threat-Intelligence 遥测的所有进程列表如下所示:
dx -g @$cursession.Processes.Where(p => (p.KernelObject.EnableProcessImpersonationLogging == 1 || p.KernelObject.EnableProcessLocalExecProtectVmLogging == 1) || p.KernelObject.EnableProcessRemoteExecProtectVmLogging == 1 || p.KernelObject.EnableProcessSuspendResumeLogging == 1 || p.KernelObject.EnableReadVmLogging == 1 || p.KernelObject.EnableThreadSuspendResumeLogging == 1 || p.KernelObject.EnableWriteVmLogging == 1).Select(p => new { Name = p->Name, EnableProcessImpersonationLogging = p.KernelObject.EnableProcessImpersonationLogging, EnableProcessLocalExecProtectVmLogging = p.KernelObject.EnableProcessLocalExecProtectVmLogging, EnableProcessRemoteExecProtectVmLogging = p.KernelObject.EnableProcessRemoteExecProtectVmLogging, EnableProcessSuspendResumeLogging = p.KernelObject.EnableProcessSuspendResumeLogging, EnableReadVmLogging = p.KernelObject.EnableReadVmLogging, EnableThreadSuspendResumeLogging = p.KernelObject.EnableThreadSuspendResumeLogging, EnableWriteVmLogging = p.KernelObject.EnableWriteVmLogging }),d
结论
我们已就本文发现与 Microsoft 进行了协调,MSRC 的结论是:由于管理员 <-> PPL 这一边界本身不可强制执行,因此不存在可被认定为漏洞的问题。SecurityTrace是一个相当晦涩且未公开的标志位;我们在 Origin (By Prelude) 公司内部为更好地保护客户而开展研究时,发现它颇具研究价值。若没有任何建议,本文也不算完整:我们的建议是将对 SecurityTracetrace 的检查移入内核,而不是委托给用户态。感谢阅读,希望你喜欢这篇文章!
CHECK YOUR PRIVILEGE: THE CURIOUS CASE OF ETW’S SECURITYTRACE FLAG
免责声明:本博客文章仅用于教育和研究目的。提供的所有技术和代码示例旨在帮助防御者理解攻击手法并提高安全态势。请勿使用此信息访问或干扰您不拥有或没有明确测试权限的系统。未经授权的使用可能违反法律和道德准则。作者对因应用所讨论概念而导致的任何误用或损害不承担任何责任。
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:securitainment CONNOR MCGARR
CONNOR MCGARR《权限核查:ETW 的 SecurityTrace 标志奇案——无需 Antimalware-PPL 消费 Microsoft-Windows-Threat-Intelligence》