在本文中,IBM Security X-Force Red 红队研究人员针对攻击者提权后,如何借助已获取的权限部署 Windows 内核层后期渗透能力这一问题展开分析。过去数年,公开披露的案例表明,即便是技术水平相对有限的攻击者,也开始采用此类技术达成攻击目的。因此,我们亟需重点关注这项攻击能力,并深入探究其潜在危害。具体而言,本文将分析攻击者如何利用内核层后期渗透技术对 ETW 传感器实施致盲攻击,同时结合去年在真实攻击场景中捕获到的恶意软件样本,对该技术的实际应用展开关联性分析。
近年来,Windows 系统的安全防护措施与检测遥测能力已得到显著完善。当这些能力与配置得当的端点检测与响应 (EDR) 解决方案相结合时,能够对攻击者的后期渗透行动构成一道难以突破的屏障。攻击者需要持续投入成本,对其战术、技术与程序 (TTP) 进行研发与迭代,以此规避安全检测的启发式规则。而在 IBM Security X-Force 的对手模拟团队,我们也正面临着同样的挑战。团队的核心任务,是在部分规模庞大且防护严密的网络环境中,对各类高级威胁攻击能力进行模拟验证。复杂且经过精细调优的安全解决方案,再加上训练有素的安全运营中心 (SOC) 团队,会对攻击者的攻击手法形成极大的压制。在某些场景下,某一项特定的 TTP 往往在三到四个月内就会彻底失效,这通常与特定的技术架构密切相关。
攻击者可能会选择借助 Windows 内核层代码执行权限,篡改部分安全防护机制,或是直接绕过大量用户态传感器的检测。此类攻击能力的首次公开演示,可追溯至 1999 年发布于《Phrack 杂志》的相关内容。在此后的数十年间,已有多起公开案例证实,威胁参与者 (TA) 会利用内核级 Rootkit 开展后期渗透活动。其中较为典型的早期案例包括 Derusbi 家族恶意软件与 Lamberts 工具包。
传统上,这类攻击能力基本仅限高级威胁参与者掌握。然而近年来,我们发现越来越多的普通攻击者也开始利用自带易受攻击驱动程序 (BYOVD) 这一攻击载体,实现对终端的恶意操作。在某些情况下,此类技术的实现方式相当初级,仅能完成一些简单任务;但与此同时,也出现了不少功能更为完善的技术验证案例。
2022 年 9 月底,ESET 的研究人员发布了一份白皮书,披露了 Lazarus 威胁参与者在针对比利时与荷兰境内目标机构发起的多起攻击中,所采用的一项内核层攻击技术,其攻击目的为数据窃取。该白皮书详细阐述了此载荷所使用的多项内核对象直接篡改 (DKOM) 原语,这些技术被用于对操作系统、杀毒软件及终端检测与响应系统的遥测功能实施致盲。目前,针对这类技术的公开研究资料十分匮乏,因此深入理解内核层后期渗透攻击手法,对防御方而言至关重要。深入理解内核层后期渗透攻击手法,对于防御工作而言至关重要。一种常见且片面的观点认为:攻击者一旦获取提权,便可肆意妄为,因此我们无需对该场景下的攻击能力进行建模分析。但这种看法站不住脚。防御方必须明确以下几点:攻击者提权后具备哪些具体攻击能力;哪些数据源仍具备可信度(哪些已不可信);存在哪些攻击遏制方案;以及如何检测各类高级攻击技术(即便当前尚不具备实现这类检测的能力)。我将通过本文重点探讨通过篡改 Event Tracing for Windows (ETW) 内核层结构体,使 ETW 提供程序完全失效或无法运行的具体攻击技术。我将首先介绍该技术的相关背景,分析攻击者篡改内核层 ETW 结构体的具体手法,并深入阐述定位这些结构体的技术原理。最后,本文还将剖析 Lazarus 组织如何在其载荷中实现该攻击技术。
ETW 是 Windows 操作系统内置的一套高速跟踪机制。它支持应用程序、驱动程序及操作系统对各类事件与系统活动进行日志记录,可为调试排障、性能分析及安全诊断工作,提供对系统行为的全面可视性。
在本节中,我将对内核层 ETW 及其对应的攻击面进行概述。掌握这些内容,有助于更好地理解篡改 ETW 提供程序的底层技术原理,以及此类篡改操作所引发的相关影响。
Binarly 研究人员曾在 2021 年 BHEU 大会上发表演讲,探讨了 Windows 系统中 ETW 机制的通用攻击面。下文将展示该威胁模型的概览图。
在这篇文章中,我们将重点关注内核层的攻击面。
本文仅探讨图 2 所示第一类攻击手段,即通过各类方式禁用或篡改 ETW 跟踪功能的攻击。
需要特别提醒的是,在研究 Windows 系统中的不透明结构体时,必须时刻谨记:这类结构体的定义会随系统版本迭代发生变更,且事实上在不同 Windows 版本中往往存在频繁改动。这一点在对内核数据执行篡改操作时尤为关键,任何操作失误都极有可能导致系统蓝屏死机 (BSoD),务必谨慎操作!
内核提供程序通过 nt!EtwRegister 函数完成注册,该函数由 ntoskrnl 导出。下文展示了该函数的反编译代码。
完整的初始化流程在内部函数 EtwpRegisterKMProvider 中完成,不过我们可以从该流程中提炼出两个核心要点:
下面,我们简要罗列 Binarly 在图 2 的幻灯片中重点提及的各类结构体。
下文展示了 _ETW_REG_ENTRY 结构的完整 64 位定义清单。更多详细信息可参见 Geoff Chappell 的博客。也可通过 Vergilius Project 进一步研究该结构。
// 0x70 bytes (sizeof)
// Win11 22H2 10.0.22621.382
struct _ETW_REG_ENTRY
{
struct _LIST_ENTRY RegList; //0x0
struct _LIST_ENTRY GroupRegList; //0x10
struct _ETW_GUID_ENTRY* GuidEntry; //0x20
struct _ETW_GUID_ENTRY* GroupEntry; //0x28
union
{
struct _ETW_REPLY_QUEUE* ReplyQueue; //0x30
struct _ETW_QUEUE_ENTRY* ReplySlot[4]; //0x30
struct
{
VOID* Caller; //0x30
ULONG SessionId; //0x38
};
};
union
{
struct _EPROCESS* Process; //0x50
VOID* CallbackContext; //0x50
};
VOID* Callback; //0x58
USHORT Index; //0x60
union
{
USHORT Flags; //0x62
struct
{
USHORT DbgKernelRegistration:1; //0x62
USHORT DbgUserRegistration:1; //0x62
USHORT DbgReplyRegistration:1; //0x62
USHORT DbgClassicRegistration:1; //0x62
USHORT DbgSessionSpaceRegistration:1; //0x62
USHORT DbgModernRegistration:1; //0x62
USHORT DbgClosed:1; //0x62
USHORT DbgInserted:1; //0x62
USHORT DbgWow64:1; //0x62
USHORT DbgUseDescriptorType:1; //0x62
USHORT DbgDropProviderTraits:1; //0x62
};
};
UCHAR EnableMask; //0x64
UCHAR GroupEnableMask; //0x65
UCHAR HostEnableMask; //0x66
UCHAR HostGroupEnableMask; //0x67
struct _ETW_PROVIDER_TRAITS* Traits; //0x68
};
_ETW_REG_ENTRY 结构的嵌套成员之一为 GuidEntry,其类型是 _ETW_GUID_ENTRY 结构。关于这一未公开结构的更多信息,可查阅 Geoff Chappell 的博客,以及 Vergilius Project。
// 0x1a8 bytes (sizeof)
// Win11 22H2 10.0.22621.382
struct _ETW_GUID_ENTRY
{
struct _LIST_ENTRY GuidList; //0x0
struct _LIST_ENTRY SiloGuidList; //0x10
volatile LONGLONG RefCount; //0x20
struct _GUID Guid; //0x28
struct _LIST_ENTRY RegListHead; //0x38
VOID* SecurityDescriptor; //0x48
union
{
struct _ETW_LAST_ENABLE_INFO LastEnable; //0x50
ULONGLONG MatchId; //0x50
};
struct _TRACE_ENABLE_INFO ProviderEnableInfo; //0x60
struct _TRACE_ENABLE_INFO EnableInfo[8]; //0x80
struct _ETW_FILTER_HEADER* FilterData; //0x180
struct _ETW_SILODRIVERSTATE* SiloState; //0x188
struct _ETW_GUID_ENTRY* HostEntry; //0x190
struct _EX_PUSH_LOCK Lock; //0x198
struct _ETHREAD* LockOwner; //0x1a0
};
最后,_ETW_GUID_ENTRY 结构的嵌套成员之一为 ProviderEnableInfo,其类型是 _TRACE_ENABLE_INFO 结构。如需了解该数据结构的成员细节,可参考 Microsoft 官方文档以及 Vergilius Project。该结构中的配置参数,会直接影响 ETW 提供程序的运行状态与功能能力。
// 0x20 bytes (sizeof)
// Win11 22H2 10.0.22621.382
struct _TRACE_ENABLE_INFO
{
ULONG IsEnabled; //0x0
UCHAR Level; //0x4
UCHAR Reserved1; //0x5
USHORT LoggerId; //0x6
ULONG EnableProperty; //0x8
ULONG Reserved2; //0xc
ULONGLONG MatchAnyKeyword; //0x10
ULONGLONG MatchAllKeyword; //0x18
};
理论背景知识固然重要,但想要深入理解一个技术主题,最佳方式始终是结合具体的应用案例来分析。接下来我们简要看一个示例。大多数关键的内核态 ETW 提供程序,均是在未导出函数 nt!EtwpInitialize 中完成初始化的。对该函数进行分析可以发现,它初始化了约 15 个提供程序。
我们以 Microsoft-Windows-Threat-Intelligence (EtwTi) 注册项为例,可通过查询全局参数 ThreatIntProviderGuid 来还原该提供程序对应的 GUID。
在网络上检索该 GUID,即可直接验证我们还原出了正确的数值 (f4e1897c-bb5d-5668-f1d8-040f4d8dd344)。
接下来我们分析注册句柄参数 EtwThreatIntProvRegHandle 的实际应用场景与使用逻辑。该句柄被引用的其中一处是函数 nt!EtwTiLogDriverObjectUnLoad。从函数名称即可直观判断,其作用是在内核卸载驱动对象时生成对应的 ETW 事件。
此处调用了 nt!EtwEventEnabled 和 nt!EtwProviderEnabled 两个函数,且均将该注册句柄作为入参传入。接下来我们分析其中一个子函数,以深入理解其底层执行逻辑。
诚然,这有点难以理解。不过,指针运算并不是特别重要。我们将重点分析该函数对注册句柄的处理逻辑。可以看到,此函数会对 _ETW_REG_ENTRY 结构体及其子结构体(例如 GuidEntry 属性)的多项属性执行有效性校验。
struct _ETW_REG_ENTRY
{
…
struct _ETW_GUID_ENTRY* GuidEntry; //0x20
…
}
还有 GuidEntry->ProviderEnableInfo 属性。
struct _ETW_GUID_ENTRY
{
…
struct _TRACE_ENABLE_INFO ProviderEnableInfo; //0x60
…
}
随后该函数会执行一系列类似的基于级别的校验。最终,函数返回布尔值,用以标识该提供程序是否针对指定级别和关键字用了事件日志记录功能。更多细节可参考 Microsoft 官方文档。
由此可见,当通过注册句柄访问某个提供程序时,其关联结构体的完整性会直接决定该提供程序能否正常运行。反之,一旦攻击者能够篡改这些结构体,就可以篡改调用方的执行流程,进而实现事件日志的丢弃或彻底屏蔽。
结合前文提及的 Binarly 所定义的攻击面,再辅以我们上述的浅层分析,即可推导得出若干能够破坏事件采集功能的攻击策略。
我们现在已经清晰掌握了针对 ETW 的 DKOM 攻击的基本原理。假设攻击者已通过某种漏洞获得了内核读写原语(正如本次案例中的 Lazarus 恶意软件,就是通过加载存在漏洞的驱动实现这一目的)。目前攻击者尚缺的关键环节,是找到这些注册句柄的方法。
我将介绍两种定位这些句柄的核心技术方法,并阐述其中一种方法的变体,该变体已被 Lazarus 组织应用于其内核载荷中。
首先,我们有必要先明确一点:尽管系统部署了内核 KASLR 机制,但对于能够以 MedIL 或更高权限执行代码的本地攻击者而言,该机制并不能构成一道有效的安全边界。存在多种可泄露内核指针的方法,而这些方法仅会在沙盒环境或 LowIL 场景下受到限制。若需了解相关技术背景,可参考 Alex Ionescu 发表的文章《我遇到的麻烦千千万,内核指针泄露却从不算》,文中提及的诸多技术手段至今仍具备实用性。
此处选用的工具是调用 ntdll!NtQuerySystemInformation 函数,并指定 SystemModuleInformation 类别:
internal static UInt32 SystemModuleInformation = 0xB;
[DllImport(“ntdll.dll”)]
internal static extern UInt32 NtQuerySystemInformation(
UInt32 SystemInformationClass,
IntPtr SystemInformation,
UInt32 SystemInformationLength,
ref UInt32 ReturnLength);
该函数会返回内核空间中所有已加载模块的实时基地址。基于这些基地址,攻击者可解析磁盘上对应的模块文件,实现原始文件偏移量与相对虚拟地址之间的双向转换。
public static UInt64 RvaToFileOffset(UInt64 rva, List<SearchTypeData.IMAGE_SECTION_HEADER> sections)
{
foreach (SearchTypeData.IMAGE_SECTION_HEADER section in sections)
{
if (rva >= section.VirtualAddress && rva < section.VirtualAddress + section.VirtualSize)
{
return (rva – section.VirtualAddress + section.PtrToRawData);
}
}
return 0;
}
public static UInt64 FileOffsetToRVA(UInt64 fileOffset, List<SearchTypeData.IMAGE_SECTION_HEADER> sections)
{
foreach (SearchTypeData.IMAGE_SECTION_HEADER section in sections)
{
if (fileOffset >= section.PtrToRawData && fileOffset < (section.PtrToRawData + section.SizeOfRawData))
{
return (fileOffset – section.PtrToRawData) + section.VirtualAddress;
}
}
return 0;
}
攻击者也可通过标准的加载库 API(例如 ntdll!LdrLoadDll),将这些内核模块加载至自身的用户态进程中。这种方式能够省去文件偏移量与相对虚拟地址 (RVA) 之间来回转换的繁琐操作。但从操作安全的角度来看,这种方法并非最优选择,因为它会产生更多可供检测的遥测数据。
在条件允许的情况下,这是我更倾向采用的技术方案,因为该方法能提升内核指针泄露功能在不同模块版本间的可移植性,其受补丁更新带来的变动影响更小。但该方案也存在一个弊端,即它的实现依赖于目标泄露对象所对应的小工具链的存在。
以 ETW 注册句柄为例,我们选取 Microsoft-Windows-Threat-Intelligence 作为分析对象。下文将展示调用 nt!EtwRegister 函数的完整代码逻辑。
此处我们的目标是泄露指向该注册句柄的指针 EtwThreatIntProvRegHandle。如图 8 第一行所示,该指针被加载到 param_4 中。此指针最终指向内核模块 .data 节区中的一个全局变量。由于该调用发生在未导出函数内,我们无法直接泄露其地址。取而代之的思路是,查找这个全局变量被哪些函数引用,并确认这些引用函数中是否存在地址可被泄露的目标。
对这些引用项展开排查后,很快就能定位到一个候选目标,即 nt!KeInsertQueueApc 函数。
该函数之所以是理想的候选目标,主要基于以下几点原因:
查看对应的汇编代码,其指令布局如下。
泄露该注册句柄的操作至此变得十分简单。我们利用已获取的内核漏洞读取一段字节数组,接着查找第一条 mov R10 指令,以此计算出该全局变量的相对虚拟偏移量。计算逻辑如下:
Int32 pOffset = Marshal.ReadInt32((IntPtr)(pBuff.ToInt64() + i + 3));
hEtwTi = (IntPtr)(pOffset + i + 7 + oKeInsertQueueApc.pAddress.ToInt64());
一旦获取该注册句柄,攻击者便能进一步访问 _ETW_REG_ENTRY 数据结构。
总体而言,这类小工具链可被用于泄露多种内核数据结构。但值得指出的是,并非总能找到此类小工具链,且部分小工具链可能包含多个复杂的执行阶段。例如,一条用于泄露页目录项 (PDE) 常量的小工具链,其结构可能如下所示。
MmUnloadSystemImage -> MiUnloadSystemImage -> MiGetPdeAddress
事实上,对 ETW 注册句柄展开的粗略分析显示,大多数注册句柄并不存在前文所述的可用小工具链。
泄露这类 ETW 注册句柄的另一种核心方案,是采用内存扫描,既可直接扫描实时内核内存,也可对磁盘中的内核模块文件进行扫描。需要注意的是,若对磁盘模块文件执行扫描,可借助前文提及的方法实现文件偏移量与相对虚拟地址之间的转换。
该方法的核心流程为:识别唯一字节特征码 → 扫描内存中匹配该特征码的位置 → 在特征码匹配位置的偏移处执行后续操作。为更清晰地理解这一思路,我们再回看 nt!EtwpInitialize 函数:
该函数中对 nt!EtwRegister 的全部十五次调用基本集中在一起。此处的核心策略是:找到一个出现在第一次 nt!EtwRegister 调用之前的唯一特征码,以及另一个出现在最后一次 nt!EtwRegister 调用之后的特征码。这一操作并不复杂。提升该方法可移植性的一个技巧是,开发一款能够处理通配符字节串的特征码扫描器。具体实现可由读者自行完成。一
旦确定了起始和终止位置的索引,便可解析两者之间的所有指令。
一旦定位到所有 CALL 指令,便可以向前回溯指令流,提取出该函数的两个关键参数:一是用于标识提供程序的 GUID,二是注册句柄的内存地址。掌握这些信息后,攻击者即可对注册句柄实施有针对性的 DKOM 攻击,从而篡改目标提供程序的运行逻辑。
我获取了 ESET 白皮书中提及的 FudModle DLL 样本并展开了分析。该 DLL 会加载一个经过数字签名但存在漏洞的戴尔驱动程序(该驱动从经内联 XOR 编码的资源段中提取),随后借助该驱动篡改内核中的大量数据结构,以此限制主机上的遥测数据采集。
作为本文的最后一部分,我将梳理 Lazarus 组织所采用的内核 ETW 注册句柄查找策略。该策略是前文所述扫描方法的一种变体。
在搜索函数的起始阶段,Lazarus 组织会先解析出 nt!EtwRegister 函数的地址,并以此作为扫描的起始点。
这一做法其实有些反常,因为它的有效性取决于函数的存储位置与其被调用位置之间的关系。函数在模块中的相对位置可能会随版本迭代发生变化,毕竟代码会有新增、移除或修改的情况。不过,基于模块的编译机制,函数之间通常会保持相对稳定的排列顺序。据此推测,采用这种策略应该是为了优化扫描速度。
在 ntoskrnl 中查找 nt!EtwRegister 的引用关系时可以发现,采用这种策略并不会遗漏太多引用项。Lazarus 或许还开展了额外分析,从而判定这些被遗漏的引用项无关紧要,无需对其进行补丁篡改。下文将对这些被遗漏的引用项加以标注。通过该策略,Lazarus 在扫描过程中可直接跳过 0x7b1de0 字节的内存空间;若扫描器的运行速度较慢,这一优化节省的开销将十分可观。
此外,在启动扫描时,会先跳过前五个匹配项,之后才开始记录注册句柄。该搜索函数的部分实现逻辑如下所示。
这段代码虽略显晦涩,但核心逻辑清晰可辨。代码会定位所有调用 nt!EtwRegister 的指令位置,提取对应的注册句柄,通过 KASLR 绕过技术将该句柄转换为内核实时内存地址,最终将这个句柄指针存储至恶意软件配置结构体中专门预留的数组内(该结构体在初始化阶段完成分配)。
最后,让我们来看看 Lazarus 如何禁用这些提供程序。
这一操作的逻辑基本清晰:Lazarus 会先泄露我们前文提及的全局变量,随后将该地址上的指针覆写为空值。如此一来,若 _ETW_REG_ENTRY 数据结构原本存在引用关系,便会被彻底清除。
不过,我对这种攻击手法的技术实现并不完全认可,原因如下:
出于技术研究目的,我重新实现了这一攻击手法;但同时我对其中的技术细节做了若干优化调整。
总体而言,经优化调整后,上述技术无疑是实现此类枚举操作的最优方案。得益于优化算法的加持,搜索耗时已可忽略不计,因此直接扫描磁盘上的完整内核模块文件,再通过额外的扫描后处理逻辑筛选结果,会是更合理的选择。
有必要简要评估此类攻击的实际影响程度。当提供程序的数据被削减甚至完全阻断时,系统的信息采集能力会出现缺失;但与此同时,并非所有这些提供程序都会上报与安全相关的敏感事件。
但在这些提供程序中,有一部分属于安全敏感类。最典型的例子就是 Microsoft-Windows-Threat-Intelligence (EtwTi),它曾是 Microsoft Defender Advanced Threat Protection (MDATP)(现更名为 Defender for Endpoint,命名变动确实容易造成混淆)的核心数据来源。需要特别指出的是,该提供程序的访问权限受到严格管控:仅早期启动反恶意软件 (ELAM) 驱动能够向其完成注册;与此同时,接收该提供程序事件的用户态进程,必须具备受保护状态 (ProtectedLight / Antimalware),且需使用与 ELAM 驱动相同的数字证书完成签名。
借助 EtwExplorer,我们能够更清晰地了解该提供程序可上报的各类信息类型。
这份 XML 事件清单的内容体量过大,无法在此完整附上;不过,下文展示了其中一个事件示例,可借此了解通过 DKOM 攻击能够抑制的具体数据类型。
内核一直是,且未来仍会是关键的攻防对抗领域,Microsoft 及第三方安全供应商均需投入精力保障操作系统的完整性。内核数据篡改不仅是漏洞利用后的攻击手段,更是内核漏洞开发过程中的核心环节。Microsoft 在该领域已取得诸多进展,例如推出了基于虚拟化的安全技术 (VBS),以及其组件之一的内核数据保护 (KDP)。
相应地,Windows 操作系统的使用者需确保充分利用这些技术成果,以此最大限度地增加潜在攻击者的攻击成本。Windows Defender Application Control (WDAC) 可被用于两大核心场景:一是确保 VBS 相关防护机制处于生效状态;二是配置并强制执行相关策略,禁止加载存在潜在风险的驱动程序。
随着越来越多的通用型威胁参与者开始利用 BYOVD 攻击,在内核空间实施 DKOM,上述防护措施的重要性愈发凸显。