John Uhlmann

内核 ETW 是最好的 ETW

本研究侧重于原生审计日志在安全设计软件中的重要性,强调需要使用内核级 ETW 日志而不是用户模式钩子来增强防篡改保护。

阅读时长 14 分钟观点
Kernel ETW is the best ETW

序言

安全设计软件的一个关键特性是,在执行特权操作时生成审计日志。这些原生审计日志可以包括内部软件状态的详细信息,而这些信息对于第三方安全供应商来说,事后添加是不切实际的。

大多数 Windows 组件都使用Windows 事件跟踪 (ETW) 生成日志。这些事件公开了 Windows 的一些内部运作,并且在某些情况下,端点安全产品会受益于订阅它们。但是,出于安全目的,并非所有的 ETW 提供程序都是一样的。

首先要考虑的是事件提供程序本身的可靠性——特别是日志记录发生的位置。它是在客户端进程中吗,并且很容易受到 ETW 篡改的攻击?或者它可能在 RPC 服务器进程中稍微安全一些?理想情况下,遥测数据将来自内核。考虑到用户到内核的安全边界,这比进程内遥测提供更强的防篡改保证。这是微软推荐的方法。与 Elastic Endpoint 一样,Microsoft Defender for Endpoint 也使用内核 ETW,而不是脆弱的用户模式 ntdll 钩子。

例如,攻击者可能很容易避开 ntdll!NtProtectVirtualMemory 上的进程内用户模式钩子,但绕过内核 PROTECTVM ETW 事件则要困难得多。或者,至少应该是这样

安全事件日志实际上只是来自 Microsoft-Windows-Security-Auditing ETW 提供程序的事件的持久存储。令人惊讶的是,进程创建的安全事件 4688 不是内核事件。内核将数据分派到本地安全机构 (lsass.exe) 服务,并发出一个 ETW 事件供事件日志使用。因此,数据可能会在该服务器进程中被篡改。与之形成对比的是,来自 Microsoft-Windows-Kernel-Process 提供程序的 ProcessStart 事件,该事件由内核直接记录,并且需要内核级权限才能进行干扰。

其次要考虑的是所记录信息的可靠性。你可能信任事件源,但如果它只是盲目记录客户端提供的与所记录事件无关的外部数据呢?

在本文中,我们将重点关注内核 ETW 事件。这些事件通常与安全性最相关,因为它们难以绕过,并且通常与代表客户端线程执行的特权操作有关。

当 Microsoft 引入内核补丁保护时,安全供应商在监控内核的能力方面受到了很大的限制。考虑到 Microsoft 提供的有限数量的内核扩展点,他们越来越被迫依赖异步 ETW 事件,以便事后了解代表恶意软件执行的内核操作。

鉴于这种依赖性,Windows 内核遥测源的公开文档不幸地有些稀疏。

内核 ETW 事件

目前有四种类型的 ETW 提供程序我们需要考虑。

首先,有“事件提供程序”的传统和现代变体

  • 传统(基于 mof)事件提供程序
  • 现代(基于清单)事件提供程序

然后是“跟踪提供程序”的传统和现代变体

  • 传统 Windows 软件跟踪预处理器 (WPP) 跟踪提供程序
  • 现代 TraceLogging 跟踪提供程序

“事件”与“跟踪”之间的区别主要是语义上的。事件提供程序通常会提前在操作系统中注册,你可以检查可用的遥测元数据。这些通常由系统管理员用于故障排除目的,并且通常是半文档化的。但是,当出现非常、非常严重的问题时,会有(隐藏的)跟踪提供程序。这些通常仅由原始软件作者用于高级故障排除,并且没有文档记录。

在实践中,每个都使用稍微不同的格式文件来描述和注册其事件,这在事件的记录方式上引入了微小的差异,更重要的是,引入了潜在事件的枚举方式的差异。

现代内核事件提供程序

现代内核 ETW 提供程序没有严格的文档记录。但是,可以通过跟踪数据帮助程序 API从操作系统查询已注册的事件详细信息。微软的PerfView 工具使用这些 API 重构提供程序的注册清单,而 Pavel Yosifovich 的 EtwExplorer 然后将这些清单包装在简单的 GUI 中。你可以使用这些来自连续 Windows 版本的已注册清单的制表符分隔值文件。每行一个事件对于 grepping 非常有用,尽管其他人已经发布了原始 XML 清单

但这并不是所有可能的 Windows ETW 事件。它们只是默认在操作系统中注册的事件。例如,在启用该功能之前,许多服务器角色的 ETW 事件不会注册

传统内核事件提供程序

微软记录了传统内核事件。大部分是这样的。

传统提供程序也以 WMI EventTrace 类形式存在于操作系统中。提供程序是根类,组是子类,而事件是孙类。

为了以与现代事件相同的方式搜索传统事件,这些类被解析,并重建了原始 MOF(大部分)。EtwExplorer 中添加了此 MOF 支持,并且制表符分隔值摘要的传统事件是这些类被解析,并重建了原始 MOF(大部分)。EtwExplorer 中添加了此 MOF 支持,并且发布了制表符分隔值摘要的传统事件。

完全重建的 Windows 内核跟踪 MOF 在此处 (或以表格格式 此处)。

在 340 个已注册的旧版事件中,只有 116 个被记录在案。通常,每个旧版事件都需要通过特定的标志启用,但这些标志也没有被记录。在内核对象管理器跟踪事件的文档中有一个线索。它提到了 PERF_OB_HANDLE,这是一个在最新 SDK 的头文件中没有定义的常量。幸运的是,Geoff Chappell 和 Windows 10 1511 WDK 提供了帮助。此信息用于将 PERFINFO_GROUPMASK 内核跟踪标志添加到 Microsoft 的 KrabsETW 库中。结果还发现,对象跟踪文档是错误的。该非公开常量只能与未记录的 API 扩展一起使用。幸运的是,像 PerfView 这样的公共 Microsoft 项目通常会提供 如何使用未记录的 API 的示例

由于清单和 MOF 都已在 GitHub 上发布,现在可以使用 此查询找到大多数内核事件。

有趣的是,Microsoft 经常混淆与安全相关的事件的名称,因此搜索具有通用名称前缀(如 task_)的事件会产生一些有趣的结果

有时,关键字会提示事件的用途。例如,Microsoft-Windows-Kernel-General 中的 task_014 通过关键字 KERNEL_GENERAL_SECURITY_ACCESSCHECK 启用。

值得庆幸的是,参数几乎总是被很好地命名。我们可以猜测 Microsoft-Windows-Kernel-Audit-API-Calls 中的 task_05OpenProcess 相关,因为它会记录名为 TargetProcessIdDesiredAccess 的字段。

另一个有用的查询是搜索具有显式 ProcessStartKey 字段的事件。可以配置 ETW 事件,以便为日志记录进程包含此字段,并且任何为另一个进程包含此信息的事件通常都与安全相关。

如果您心中有一个特定的 API,您可以查询它的名称或其参数。例如,如果您想要命名管道事件,您可以使用 此查询

但是,在本例中,Microsoft-Windows-SEC 属于 Microsoft Defender for Endpoint (MDE) 使用的内置 Microsoft 安全驱动程序。此提供程序仅正式提供给 MDE,尽管 Sebastian Feldmann 和 Philipp Schmied 已经演示了如何使用 AutoLogger 启动会话并订阅该会话的事件。这目前仅对 MDE 用户有用,否则驱动程序不会配置为发出事件。

但是,跟踪提供程序呢?

现代内核跟踪提供程序

TraceLogging 元数据作为不透明的 blob 存储在日志记录二进制文件中。幸运的是,Matt Graeber 已经对这种格式进行了逆向工程。我们可以使用 Matt 的脚本转储 ntoskrnl.exe 的所有 TraceLogging 元数据。Windows 11 TraceLogging 元数据的示例转储在此处

不幸的是,仅元数据结构无法保留提供程序和事件之间的关联。有一些有趣的提供程序名称,例如 Microsoft.Windows.Kernel.SecurityAttackSurfaceMonitor,但从我们的元数据转储中尚不清楚哪些事件属于这些提供程序。

旧版内核跟踪提供程序

WPP 元数据存储在符号文件 (PDB) 中。Microsoft 将此信息包含在一些驱动程序(但并非所有驱动程序)的公共符号中。但是,内核本身不生成任何 WPP 事件。相反,可以向旧版 Windows 内核跟踪事件提供程序传递未记录的标志,以启用通常仅 Microsoft 内核开发人员可用的旧版“跟踪”事件。

提供程序文档事件元数据
现代事件提供程序已注册的 XML 清单
旧版事件提供程序部分EventTrace WMI 对象
现代跟踪提供程序二进制文件中的未记录的 blob
旧版跟踪提供程序符号中的未记录的 blob

后续步骤

我们现在拥有四种 ETW 提供程序的所有内核事件元数据,但 ETW 事件列表只是我们的起点。了解提供程序和事件关键字可能不足以生成我们期望的事件。有时,需要额外的配置注册表项或 API 调用。但是,更多时候,我们只需要了解记录事件的确切条件。

确切了解记录的内容和位置对于真正了解遥测及其局限性至关重要。而且,由于反编译器变得容易获得,我们可以选择一些足够的反向工程。在 IDA 中,我们将其称为“按 F5”。Ghidra 是开源的替代方案,它支持脚本...使用 Java。

对于内核 ETW,我们特别感兴趣的是从系统调用可访问的 EtwWrite 调用。我们希望获得尽可能多的调用站点参数信息,包括任何相关的公共符号信息。这意味着我们需要遍历调用图,但也要尝试解析特定参数的可能值。

必要的参数是 RegHandleEventDescriptor。前者是提供程序的不透明句柄,后者提供事件特定信息,例如事件 ID 及其关联的关键字。ETW 关键字是用于启用一组事件的标识符。

更好的是,这些事件描述符通常存储在具有公共符号的全局常量中。

我们有足够的事件元数据,但仍然需要将运行时分配的不透明提供程序句柄解析回有关提供程序的元数据。为此,我们还需要 EtwRegister 调用。

内核现代事件提供程序的典型模式是将常量提供程序 GUID 和运行时句柄存储在具有公共符号的全局变量中。

遇到的另一种模式是在同一函数中调用 EtwRegisterEtwEwriteEtwUnregister。在这种情况下,我们利用局部性来查找事件的提供程序 GUID。

但是,现代 TraceLogging 提供程序没有关联的每个提供程序的公共符号来提示每个提供程序的用途。但是,Matt Graeber 已经逆向了 TraceLogging 元数据格式,并记录了提供程序名称存储在距提供程序 GUID 的固定偏移量处。拥有确切的提供程序名称甚至比我们为现代事件恢复的公共符号更好。

这只剩下旧版提供程序了。它们似乎没有公共符号或元数据 blob。一些常量被传递给名为 EtwTraceKernelEvent 的未记录的函数,该函数包装了最终的 ETW 写入调用。

这些常量存在于 Windows 10 1511 WDK 标头(和System Informer 标头)中,因此我们可以使用常量名称标记这些事件。

此脚本最近已针对 Ghidra 11 进行了更新,并改进了对 TraceLogging 和旧版事件的支持。您现在可以在 GitHub 上找到它 - https://github.com/jdu2600/API-To-ETW

Windows 11 内核的示例输出在此处

我们以前匿名的 Microsoft-Windows-Kernel-Audit-API-Calls 事件很快就被此脚本揭示。

IdEVENT_DESCRIPTOR 符号函数
1KERNEL_AUDIT_API_PSSETLOADIMAGENOTIFYROUTINEPsSetLoadImageNotifyRoutineEx
2KERNEL_AUDIT_API_TERMINATEPROCESSNtTerminateProcess
3KERNEL_AUDIT_API_CREATESYMBOLICLINKOBJECTObCreateSymbolicLink
4KERNEL_AUDIT_API_SETCONTEXTTHREADNtSetContextThread
5KERNEL_AUDIT_API_OPENPROCESSPsOpenProcess
6KERNEL_AUDIT_API_OPENTHREADPsOpenThread
7KERNEL_AUDIT_API_IOREGISTERLASTCHANCESHUTDOWNNOTIFICATIONIoRegisterLastChanceShutdownNotification
8KERNEL_AUDIT_API_IOREGISTERSHUTDOWNNOTIFICATIONIoRegisterShutdownNotification

Microsoft-Windows-Kernel-Audit-API-Calls 事件的符号和包含函数

通过脚本恢复的调用路径和参数信息,我们还可以看到,早前的 SECURITY_ACCESSCHECK 事件与 SeAccessCheck 内核 API 相关联,但仅在名为 SeLogAccessFailure 的函数中记录。仅记录失败条件是 ETW 事件中非常常见的情况。对于故障排除,即原始的 ETW 用例,这些通常是最有用的,并且大多数组件中的实现都反映了这一点。不幸的是,出于安全目的,情况往往相反。成功的操作日志通常更有助于发现恶意活动。因此,某些遗留事件的价值通常较低。

现代安全设计实践是对安全相关活动的成功和失败都进行审计日志记录,微软也在不断添加新的安全相关的 ETW 事件来实现这一点。例如,Windows 11 24H2 的预览版本在 Microsoft-Windows-Threat-Intelligence 提供程序中包含一些有趣的新 ETW 事件。希望这些事件能在发布前为安全厂商提供文档说明。

在有趣的 Windows 驱动程序和服务 DLL 中运行此反编译器脚本留给读者作为练习。