ETW内部机制解析:安全研究与取证
为什么ETW对Windows端点检测与响应(EDR)如此关键?
Windows事件追踪(ETW)在Windows 10和11中已成为端点检测与响应(EDR)解决方案的核心支柱。其价值在于通过安全的ETW通道为安全工具提供智能数据,这也使其成为攻击研究人员试图绕过检测的目标。
在本次深度探讨中,我们不仅讨论ETW的功能,还探索其内部工作原理,使您能够在系统上进行新颖的研究或取证分析。安全研究人员和恶意软件作者已经将ETW作为目标,开发了多种技术来篡改或绕过基于ETW的EDR、挂钩系统调用,或访问通常为反恶意软件解决方案保留的ETW提供者。最近,Lazarus集团通过禁用ETW提供者绕过了EDR检测。本文将解释ETW的工作原理及其为何成为诱人目标,并带您深入Windows内部进行激动人心的探索。
ETW内部机制概述
ETW的两个主要组件是提供者(providers)和消费者(consumers)。提供者将事件发送到ETW全局唯一标识符(GUID),事件被写入文件、内存缓冲区或两者兼有。每个Windows系统都有数百或数千个注册的提供者。我们可以通过运行命令logman query providers
查看可用提供者:
通过检查系统,我们可以看到有近1200个注册提供者:
每个ETW提供者在其清单文件中定义自己的事件,消费者使用这些文件解析提供者生成的数据。ETW提供者可能定义数百种不同的事件类型,因此我们可以从ETW获取的信息量是巨大的。这些事件大多数可以在事件查看器(Event Viewer)中看到,这是一个内置的Windows工具,用于消费ETW事件。但您只会看到部分数据。事件查看器中默认并非所有日志都启用,也不是每个日志的所有事件ID都会显示。
另一方面,我们有消费者:跟踪日志会话,从一个或多个提供者接收事件。例如,依赖ETW数据进行检测的EDR将消费来自安全相关ETW通道(如威胁情报通道)的事件。
我们可以通过性能监视器查看所有运行的ETW消费者;点击其中一个会话将显示其订阅的提供者。(您可能需要以SYSTEM身份运行才能查看所有ETW日志会话。)
接收此日志会话事件的进程列表是有用的信息,但不易获取。据我所见,根本无法从用户模式获取该信息,即使从内核模式获取,除非您非常熟悉ETW内部机制,否则也不是一件容易的事。因此,我们将看看使用WinDbg进行内核调试会话能学到什么。
查找ETW消费者进程
有几种方法可以从用户模式查找ETW日志会话的消费者。然而,它们只提供非常部分的信息,在所有情况下都不足够。因此,我们将转向内核调试器会话。从调试器获取ETW会话信息的一种方法是使用内置扩展!wmitrace
。这个非常有用的扩展允许用户调查所有运行的记录器及其属性、消费者和缓冲区。它甚至允许用户启动和停止日志会话(在实时调试器连接上)。然而,像所有旧扩展一样,它有其局限性:无法轻松自动化,并且由于是预编译的二进制文件,无法通过新功能进行扩展。
因此,我们将编写一个JavaScript脚本——脚本更易于扩展和修改,我们可以使用它们获取所需的数据,而不受旧扩展预存在功能的限制。
每个句柄包含一个指向对象的指针。例如,文件句柄将指向类型为FILE_OBJECT的内核结构。类型为EtwConsumer的对象句柄将指向一个名为ETW_REALTIME_CONSUMER的未记录数据结构。该结构包含指向打开它的进程的指针、为不同操作通知的事件、标志,以及一条(最终)将我们引回日志会话的信息——LoggerId。使用自定义脚本,我们可以扫描所有进程的句柄表,查找EtwConsumer对象的句柄。对于每个句柄,我们可以获取链接的ETW_REALTIME_CONSUMER结构并打印LoggerId:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
"use strict";
function initializeScript()
{
return [new host.apiVersionSupport(1, 7)];
}
function EtwConsumersForProcess(process)
{
let dbgOutput = host.diagnostics.debugLog;
let handles = process.Io.Handles;
try
{
for (let handle of handles)
{
try
{
let objType = handle.Object.ObjectType;
if (objType === "EtwConsumer")
{
let consumer = host.createTypedObject(handle.Object.Body.address, "nt", "_ETW_REALTIME_CONSUMER");
let loggerId = consumer.LoggerId;
dbgOutput("Process ", process.Name, " with ID ", process.Id, " has handle ", handle.Handle, " to Logger ID ", loggerId, "\n");
}
} catch (e) {
dbgOutput("\tException parsing handle ", handle.Handle, "in process ", process.Name, "!\n");
}
}
} catch (e) {
}
}
|
接下来,我们使用.scriptload
将脚本加载到调试器中,并调用我们的函数来识别哪些进程消费ETW事件:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
dx @$cursession.Processes.Select(p => @$scriptContents.EtwConsumersForProcess(p))
@$cursession.Processes.Select(p => @$scriptContents.EtwConsumersForProcess(p))
Process svchost.exe with ID 0x558 has handle 0x7cc to Logger ID 31
Process svchost.exe with ID 0x114c has handle 0x40c to Logger ID 36
Process svchost.exe with ID 0x11f8 has handle 0x2d8 to Logger ID 17
Process svchost.exe with ID 0x11f8 has handle 0x2e8 to Logger ID 3
Process svchost.exe with ID 0x11f8 has handle 0x2f4 to Logger ID 9
Process NVDisplay.Container.exe with ID 0x1478 has handle 0x890 to Logger ID 38
Process svchost.exe with ID 0x1cec has handle 0x1dc to Logger ID 7
Process svchost.exe with ID 0x1d2c has handle 0x780 to Logger ID 8
Process CSFalconService.exe with ID 0x1e54 has handle 0x760 to Logger ID 3
Process CSFalconService.exe with ID 0x1e54 has handle 0x79c to Logger ID 45
Process CSFalconService.exe with ID 0x1e54 has handle 0xbb0 to Logger ID 10
Process Dell.TechHub.Instrumentation.SubAgent.exe with ID 0x25c4 has handle 0xcd8 to Logger ID 41
Process Dell.TechHub.Instrumentation.SubAgent.exe with ID 0x25c4 has handle 0xdb8 to Logger ID 35
Process Dell.TechHub.Instrumentation.SubAgent.exe with ID 0x25c4 has handle 0xf54 to Logger ID 44
Process SgrmBroker.exe with ID 0x17b8 has handle 0x178 to Logger ID 15
Process SystemInformer.exe with ID 0x4304 has handle 0x30c to Logger ID 16
Process PerfWatson2.exe with ID 0xa60 has handle 0xa3c to Logger ID 46
Process PerfWatson2.exe with ID 0x81a4 has handle 0x9c4 to Logger ID 40
Process PerfWatson2.exe with ID 0x76f0 has handle 0x9a8 to Logger ID 47
Process operfmon.exe with ID 0x3388 has handle 0x88c to Logger ID 48
Process operfmon.exe with ID 0x3388 has handle 0x8f4 to Logger ID 49
|
虽然我们仍然没有获取日志会话的名称,但我们已经获得了比用户模式更多的数据。例如,我们可以看到一些进程有多个消费者句柄,因为它们订阅了多个日志会话。不幸的是,ETW_REALTIME_CONSUMER结构除了其标识符外没有任何关于日志会话的信息,因此我们必须找到一种方法将标识符与人类可读的名称匹配。
注册的记录器及其ID存储在全局记录器列表中(或者至少直到引入服务器隔离区之前是这样;现在,每个隔离的进程将有自己的单独ETW记录器,而非隔离进程将使用全局列表,本文也将使用全局列表)。全局列表存储在主机隔离区全局变量nt!PspHostSiloGlobals内的ETW_SILODRIVERSTATE结构中:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
dx ((nt!_ESERVERSILO_GLOBALS*)&nt!PspHostSiloGlobals)->EtwSiloState
((nt!_ESERVERSILO_GLOBALS*)&nt!PspHostSiloGlobals)->EtwSiloState : 0xffffe38f3deeb000 [Type: _ETW_SILODRIVERSTATE *]
[+0x000] Silo : 0x0 [Type: _EJOB *]
[+0x008] SiloGlobals : 0xfffff8052bd489c0 [Type: _ESERVERSILO_GLOBALS *]
[+0x010] MaxLoggers : 0x50 [Type: unsigned long]
[+0x018] EtwpSecurityProviderGuidEntry [Type: _ETW_GUID_ENTRY]
[+0x1c0] EtwpLoggerRundown : 0xffffe38f3deca040 [Type: _EX_RUNDOWN_REF_CACHE_AWARE * *]
[+0x1c8] EtwpLoggerContext : 0xffffe38f3deca2c0 [Type: _WMI_LOGGER_CONTEXT * *]
[+0x1d0] EtwpGuidHashTable [Type: _ETW_HASH_BUCKET [64]]
[+0xfd0] EtwpSecurityLoggers [Type: unsigned short [8]]
[+0xfe0] EtwpSecurityProviderEnableMask : 0x3 [Type: unsigned char]
[+0xfe4] EtwpShutdownInProgress : 0 [Type: long]
[+0xfe8] EtwpSecurityProviderPID : 0x798 [Type: unsigned long]
[+0xff0] PrivHandleDemuxTable [Type: _ETW_PRIV_HANDLE_DEMUX_TABLE]
[+0x1010] RTBacklogFileRoot : 0x0 [Type: wchar_t *]
[+0x1018] EtwpCounters [Type: _ETW_COUNTERS]
[+0x1028] LogfileBytesWritten : {4391651513} [Type: _LARGE_INTEGER]
[+0x1030] ProcessorBlocks : 0x0 [Type: _ETW_SILO_TRACING_BLOCK *]
[+0x1038] ContainerStateWnfSubscription : 0xffffaf8de0386130 [Type: _EX_WNF_SUBSCRIPTION *]
[+0x1040] ContainerStateWnfCallbackCalled : 0x0 [Type: unsigned long]
[+0x1048] UnsubscribeWorkItem : 0xffffaf8de0202170 [Type: _WORK_QUEUE_ITEM *]
[+0x1050] PartitionId : {00000000-0000-0000-0000-000000000000} [Type: _GUID]
[+0x1060] ParentId : {00000000-0000-0000-0000-000000000000} [Type: _GUID]
[+0x1070] QpcOffsetFromRoot : {0} [Type: _LARGE_INTEGER]
[+0x1078] PartitionName : 0x0 [Type: char *]
[+0x1080] PartitionNameSize : 0x0 [Type: unsigned short]
[+0x1082] UnusedPadding : 0x0 [Type: unsigned short]
[+0x1084] PartitionType : 0x0 [Type: unsigned long]
[+0x1088] SystemLoggerSettings [Type: _ETW_SYSTEM_LOGGER_SETTINGS]
[+0x1200] EtwpStartTraceMutex [Type: _KMUTANT]
|
EtwpLoggerContext字段指向一个WMI_LOGGER_CONTEXT结构指针数组,每个结构描述一个记录器会话。数组的大小保存在ETW_SILODRIVERSTATE的MaxLoggers字段中。并非所有数组条目都必须使用;未使用的条目将设置为1。知道这一点,我们可以转储数组的所有初始化条目。(为方便起见,我已硬编码数组大小):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
dx ((nt!_WMI_LOGGER_CONTEXT*(*)[0x50])(((nt!_ESERVERSILO_GLOBALS*)&nt!PspHostSiloGlobals)->EtwSiloState->EtwpLoggerContext))->Where(l => l != 1)
((nt!_WMI_LOGGER_CONTEXT*(*)[0x50])(((nt!_ESERVERSILO_GLOBALS*)&nt!PspHostSiloGlobals)->EtwSiloState->EtwpLoggerContext))->Where(l => l != 1)
[2] : 0xffffe38f3f0c9040 [Type: _WMI_LOGGER_CONTEXT *]
[3] : 0xffffe38f3fe07640 [Type: _WMI_LOGGER_CONTEXT *]
[4] : 0xffffe38f3f0c75c0 [Type: _WMI_LOGGER_CONTEXT *]
[5] : 0xffffe38f3f0c9780 [Type: _WMI_LOGGER_CONTEXT *]
[6] : 0xffffe38f3f0cb040 [Type: _WMI_LOGGER_CONTEXT *]
[7] : 0xffffe38f3f0cb600 [Type: _WMI_LOGGER_CONTEXT *]
[8] : 0xffffe38f3f0ce040 [Type: _WMI_LOGGER_CONTEXT *]
[9] : 0xffffe38f3f0ce600 [Type: _WMI_LOGGER_CONTEXT *]
[10] : 0xffffe38f79832a40 [Type: _WMI_LOGGER_CONTEXT *]
[11] : 0xffffe38f3f0d1640 [Type: _WMI_LOGGER_CONTEXT *]
[12] : 0xffffe38f89535a00 [Type: _WMI_LOGGER_CONTEXT *]
[13] : 0xffffe38f3dacc940 [Type: _WMI_LOGGER_CONTEXT *]
[14] : 0xffffe38f3fe04040 [Type: _WMI_LOGGER_CONTEXT *]
…
|
每个记录器上下文包含有关记录器会话的信息,如其名称、存储事件的文件、安全描述符等。每个结构还包含一个记录器ID,该ID与我们刚刚转储的数组中的记录器索引匹配。因此,给定一个记录器ID,我们可以这样找到其详细信息:
1
2
3
4
5
6
7
8
9
10
11
|
dx (((nt!_ESERVERSILO_GLOBALS*)&nt!PspHostSiloGlobals)->EtwSiloState->EtwpLoggerContext)[@$loggerId]
(((nt!_ESERVERSILO_GLOBALS*)&nt!PspHostSiloGlobals)->EtwSiloState->EtwpLoggerContext)[@$loggerId] : 0xffffe38f3f0ce600 [Type: _WMI_LOGGER_CONTEXT *]
[+0x000] LoggerId : 0x9 [Type: unsigned long]
[+0x004] BufferSize : 0x10000 [Type: unsigned long]
[+0x008] MaximumEventSize : 0xffb8 [Type: unsigned long]
[+0x00c] LoggerMode : 0x19800180 [Type: unsigned long]
[+0x010] AcceptNewEvents : 0 [Type: long]
[+0x018] GetCpuClock : 0x0 [Type: unsigned __int64]
[+0x020] LoggerThread : 0xffffe38f3f0d0040 [Type: _ETHREAD *]
[+0x028] LoggerStatus : 0 [Type: long]
…
|
现在,我们可以将其实现为一个函数(在DX或JavaScript中),并为每个找到的开放消费者句柄打印记录器名称:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
dx @$cursession.Processes.Select(p => @$scriptContents.EtwConsumersForProcess(p))
@$cursession.Processes.Select(p => @$scriptContents.EtwConsumersForProcess(p))
Process svchost.exe with ID 0x558 has handle 0x7cc to Logger ID 31
Logger Name: "UBPM"
Process svchost.exe with ID 0x114c has handle 0x40c to Logger ID 36
Logger Name: "WFP-IPsec Diagnostics"
Process svchost.exe with ID 0x11f8 has handle 0x2d8 to Logger ID 17
Logger Name: "EventLog-System"
Process svchost.exe with ID 0x11f8 has handle 0x2e8 to Logger ID 3
Logger Name: "Eventlog-Security"
Process svchost.exe with ID 0x11f8 has handle 0x2f4 to Logger ID 9
Logger Name: "EventLog-Application"
Process NVDisplay.Container.exe with ID 0x1478 has handle 0x890 to Logger ID 38
Logger Name: "
|