【Windows内核】早期级联注入:Windows 进程创建、Early bird APC 注入和 EDR 预加载

news/2025/1/19 1:11:37/文章来源:https://www.cnblogs.com/o-O-oO/p/18679093

一、介绍

在这篇博客文章中,我们介绍了一种名为早期级联注入的新型进程注入技术,探讨了 Windows 进程创建,并识别了几种端点检测和响应系统(EDR)如何初始化其进程内检测能力。这种新的早期级联注入技术针对进程创建的用户模式部分,结合了众所周知的 Early bird APC 注入技术和最近由 Marcus Hutchins 发布的 EDR 预加载技术 [1]。与 Early bird APC 注入不同,这种新技术避免了跨进程异步过程调用(APC)的排队,同时最小化了远程进程交互。这使得早期级联注入成为一种隐蔽的进程注入技术,能够有效对抗顶级 EDR,同时避免检测。

为了提供对早期级联注入内部工作的见解,这篇博客还展示了用户模式进程创建流程的时间线。这个概述展示了早期级联注入的操作方式,并指出了它在进程创建中干预的确切时刻。此外,我们还将其与 EDR 用户模式检测措施的初始化时间进行了比较。

现在,让我们深入了解 Windows 进程创建、Early bird APC 注入和 EDR 预加载。一旦我们对这些主题有了扎实的理解,我们就可以继续探索早期级联注入。

二、了解 Windows 进程创建

2.1 进程创建 API

在 Windows 中,有多种 API 可以创建进程,例如 CreateProcess、CreateProcessAsUser 和 CreateProcessWithLogon,如图 1 所示。最终,这些函数都会调用 ntdll.dll 中的 NAPI NtCreateUserProcess。此函数负责通过切换控制到内核来启动进程创建,在内核中执行同名函数 NtCreateUserProcess。

这些函数都包含 dwCreationFlags 参数,该参数控制进程的创建方式。在本文中,我们将遇到 CREATE_SUSPENDED 标志,该标志指示内核在挂起状态下创建新进程的初始线程 [2]。该线程保持挂起状态,直到调用 ResumeThread 函数。

显然,这些函数还有一个参数指定要为其创建进程的应用程序的映像文件路径。有关这些 API 的其他参数和标志,请参阅 MSDN [2]。

【图 1】进程创建函数(来源:Windows Internals, Part 1)

2.2 内核模式和用户模式进程创建

进程创建分为两个部分:内核模式和用户模式。它从内核模式部分开始,由 NtCreateUserProcess 启动。一旦在内核模式下创建了进程的上下文和环境,进程的初始线程将在用户模式下完成进程创建。

内核模式部分负责打开指定应用程序的映像文件并将其映射到内存中。然后,它创建特定于进程和线程的对象,将本机库 ntdll.dll 映射到进程中,接着创建进程的初始线程。如果指定了 CREATE_SUSPENDED 标志,则该线程在挂起状态下创建,等待恢复后再将控制返回给用户模式以完成进程创建。

模块 ntdll.dll 是第一个加载到进程中的 DLL,并且是唯一在内核模式下加载的 DLL,所有其他模块都在用户模式下加载。此外,ntdll.dll 包含导出的函数 LdrInitializeThunk,该函数在应用程序的主入口点运行之前处理用户模式部分的进程创建。此函数也被称为映像加载器,ntdll.dll 中与其相关的函数以 Ldr 为前缀。

回到新创建的线程:在恢复此挂起的线程时,它开始执行 LdrInitializeThunk,即进程创建的用户模式部分。之后,新进程完全初始化并准备运行应用程序。初始线程然后开始执行应用程序的主入口点。

【注意】,使用 CREATE_SUSPENDED 标志会在初始线程切换到用户模式以运行 LdrInitializeThunk 之前暂停进程创建。这特别有趣,因为用户模式恶意软件可以在此时干预进程创建。因此,让我们仔细看看 LdrInitializeThunk 内部发生了什么。

2.3 用户模式进程创建:LdrInitializeThunk

LdrInitializeThunk 是在用户模式下执行的第一个函数,标志着恶意软件和 EDR 可以在进程中干预的初始点。我们稍后将探讨 Early bird APC 注入、EDR 预加载和早期级联注入等技术如何与 LdrInitializeThunk 交互。现在,让我们深入了解此函数的细节。

LdrInitializeThunk 是一个复杂的函数,负责进程创建的用户模式部分。它处理许多进程初始化任务,这些任务在 Windows Internals, Part 1 书中列出并简要描述。然而,该书并未涵盖 LdrInitializeThunk 内部的哪些子函数负责这些任务。因此,为了更深入地了解 LdrInitializeThunk 及其子函数,我们使用 x64dbg 和 IDA Pro 对其进行了分析。

基于此分析,我们创建了一个调用图,概述了用户模式部分进程创建的事件顺序。此时间线包括与本文相关的函数,因此省略了 LdrInitializeThunk 的某些任务和相关函数。此外,请注意,此调用图反映了我们的解释,可能并不完全准确。

LdrInitializeThunk 的调用图如下所示,随后是对可以在其中识别的任务的描述。关键函数以颜色突出显示。

LdrInitializeThunk 调用图:

此调用图展示了 LdrInitializeThunk 执行的以下任务:

1、在进程环境块(PEB)中初始化加载器锁。
PEB 是一个数据结构,存储有关进程上下文和环境的信息。稍后将讨论加载器锁。
2、设置可变只读堆部分(.mrdata)。
为 ntdll.dll 创建并插入第一个加载器数据表条目(LDR_DATA_TABLE_ENTRY)到模块数据库(PEB_LDR_DATA)中。
3、模块数据库存储在 PEB 中,包含三个列表,用于跟踪进程加载的模块:InLoadOrderModuleList、InMemoryOrderModuleList 和 InInitializationOrderModuleList。这些列表中的每个条目都包括模块的基地址、入口点和路径等详细信息。
4、初始化并行加载器。
并行加载器负责使用线程池并发加载应用程序的导入 DLL。它设置一个 LdrpWorkQueue,其中包含要加载的应用程序的第一顺序依赖项。然后,并行线程加载这些依赖项,递归依赖项被添加到工作队列中。有关 Windows 并行加载器的更多信息,请参阅这篇有见地的博客 [3]。
5、为应用程序的可执行文件创建并插入第二个加载器数据表条目(LDR_DATA_TABLE_ENTRY)到模块数据库(PEB_LDR_DATA)中。
6、加载初始模块 kernel32.dll 和 kernelbase.dll。
这些模块总是加载到每个进程中。像所有其他模块一样,这些模块也被添加到模块数据库(PEB_LDR_DATA)中。
7、如果启用,初始化 Shim 引擎并解析 Shim 数据库。默认情况下,Shim 引擎是关闭的。
Shim 引擎在不修改应用程序代码的情况下应用兼容性修复(shims)。它拦截并修改 API 调用以解决兼容性问题。有关更多详细信息,请参阅 [11]。
8、如果启用,它使用并行加载器将应用程序的其余依赖项加载到进程中;否则,DLL 将按顺序加载。

在映射和初始化所有依赖项后,LdrInitializeThunk 调用以下函数:

9、NtTestAlert:通过切换到内核模式清空调用线程的 APC 队列,然后调用 KiUserApcDispatcher 执行排队的 APC。
10、NtContinue:将初始线程的执行上下文设置为 RtlUserThreadStart,然后调用应用程序的主入口点。

在调用图的最底部,我们看到 RtlRaiseStatus。此函数通常不会执行,因为 NtContinue 将执行流重定向到应用程序的主入口点。然而,如果应用程序崩溃,则会触发 RtlRaiseStatus。

尽管调用图已简化,但仍然复杂。希望它能提供一些关于进程创建内部工作的见解。为了澄清与本文相关的关键点,我们创建了一个抽象,捕捉了您需要记住的关键信息。红色注释描述了前面函数块完成的操作。

抽象的 LdrInitializeThunk 调用图:

在此调用图的抽象中,我们观察到 LdrInitializeThunk 加载 kernel32.dll 和 kernelbase.dll,然后加载所有其他依赖项,清空其 APC 队列(NtTestAlert),最后开始执行应用程序的主入口点(NtContinue)。此外,我们观察到通过 LdrLoadDll 加载 DLL 包括两个步骤:映射和初始化。

2.4 LdrLoadDll 的工作原理

加载依赖项是 LdrInitializeThunk 的主要组成部分,这可能是它被称为映像加载器的原因。此外,EDR 预加载和早期级联注入技术在进程创建期间专门干预此 LdrLoadDll 函数。因此,我们还简要介绍了 LdrLoadDll 如何加载依赖项。
之后,我们将开始探索有趣的内容:进程注入

本节后最重要的方面是记住 LdrLoadDll 以两个步骤加载 DLL:首先通过 LdrpFindOrPrepareLoadingModule 将 DLL 的映像文件映射到内存中,然后通过 LdrpPrepareModuleForExecution 初始化 DLL。如果 DLL 有递归依赖项,这些依赖项首先映射到内存中,然后每个模块才初始化。初始化按相反顺序进行,以遵循正确的依赖顺序。

现在,让我们深入了解一些细节,并逐步了解加载 kernel32.dll 的过程,这是进程创建期间加载的第一个模块。尝试在调用图中跟随。我们从 LdrInitializeThunk 开始,它调用 LdrpLoadDll 加载 kernel32.dll。LdrpLoadDll 调用 ntdll!LdrpFindOrPrepareLoadingModule 进行第一步:将 kernel32.dll 映射到内存中。实际映射最终由嵌套函数 NtMapViewOfSection 执行。之后,LdrpMapAndSnapDependency 检查依赖项,由于 kernel32.dll 从 kernelbase.dll 导入函数,LdrpLoadDependentModule 将 kernelbase.dll 映射到内存中。

一旦这两个模块映射到内存中,LdrLoadDll 的第二部分初始化 DLL,由 LdrpPrepareModuleForExecution 执行。此函数调用 LdrpCondenseGraph 创建依赖图,存储依赖项必须初始化的顺序。接下来,LdrpInitializeGraphRecurse 处理此图,并为图中的每个模块调用 LdrpInitializeNode。图中的第一个节点是 kernelbase.dll,LdrpInitializeNode 通过 LdrpCallInitRoutine 初始化。此函数调用 kernelbase.dll 的入口点。之后,同样初始化 kernel32.dll;此后,LdrLoadDll 完成了 kernel32.dll 的加载。

三、Early bird APC 注入

现在我们对进程创建有了一个大致了解,让我们仔细看看 2018 年由 Cyberbit 发现的 Early bird APC 注入技术 [4]。这是一种众所周知且有效的进程注入方法,涉及在进程主入口点执行之前注入代码。在进程早期注入可能会绕过 EDR 检测措施,包括钩子,如果这些措施在 APC 执行之前未加载。

Early bird APC 注入技术的工作原理如下:

1、在挂起状态下创建目标进程(例如 CreateProcess);
2、在目标进程中分配可写内存(例如 VirtualAllocEx);
3、将恶意代码写入分配的内存(例如 WriteProcessMemory);
4、将指向恶意代码的 APC 排队到远程目标进程(例如 QueueUserAPC);
5、恢复目标进程,恢复时执行 APC,运行恶意代码(例如 ResumeThread)。

正如我们之前了解到的,当进程在挂起状态下创建时(1),执行在用户模式部分的进程创建之前暂停,由 LdrInitializeThunk 处理。在此时,将恶意代码的有效载荷写入目标进程(2,3)。然后,将指向有效载荷的 APC 例程排队等待在挂起线程中执行(4)。最后,恢复挂起的线程(5)。

在线程恢复时,它开始在 LdrInitializeThunk 处执行,LdrInitializeThunk 的最后一个任务之一是清空 APC 队列。具体来说,NtTestAlert 负责通过执行 APC 来清空线程的 APC 队列。这时,注入的有效载荷运行。

过去,在此时执行有效载荷足够早,可以抢先 EDR 用户模式检测措施,如钩子。然而,现代 EDR 解决方案通常在进程创建时间线的更早阶段加载其检测措施。尽管如此,我们发现一个流行的 EDR 仍在 NtTestAlert 之后加载其检测措施。对于这个特定的 EDR,Early bird APC 注入绕过了 EDR 的用户模式检测措施。尽管 Early bird APC 注入可能不再绕过现代 EDR 的钩子,但它仍然非常有用的注入目的。

Early bird APC 仍然是一种有价值的注入技术,尽管它对现代 EDR 的规避效果较差。

然而,如前所述,Early bird APC 注入可能会被检测到,因为跨进程排队 APC 的行为可疑。从一个进程到另一个进程排队 APC 被称为跨进程 APC 排队。这种行为是可疑的,并且被 EDR 密切监视。隐藏跨进程 APC 排队很困难,使其成为检测 Early bird APC 注入的强指示器。稍后,我们将看到早期级联注入如何在不进行跨进程排队的情况下执行注入,因此在我们测试的 EDR 中未被检测到。

四、EDR 预加载

早期级联注入结合了 EDR 预加载的元素。因此,让我们简要探讨一下 EDR 预加载,这是最近由 Marcus Hutchins 在博客中介绍的,他因阻止 WannaCry 而闻名 [1]。他的博客激发了我在这个领域的研究,谢谢!

EDR 预加载旨在防止 EDR 在进程创建期间加载其用户模式检测措施。例如,它可以防止 EDR 初始化其钩子 DLL,从而显著减少 EDR 在进程中的可见性,因为 EDR 无法拦截 API 调用。随着微软逐渐限制第三方对内核的访问,迫使 EDR 检测措施从内核模式转移到用户模式,这类技术变得越来越重要。据推测,由于 Crowdstrike 事件导致 850 万台 Windows 系统因其内核级软件中的错误更新而崩溃,内核限制将进一步推进 [5]。

EDR 预加载的工作原理:它首先在挂起状态下创建一个进程,并劫持 ntdll!AvrfpAPILookupCallbackRoutine 回调指针。劫持涉及将恶意代码的起始地址分配给回调指针,并通过将 ntdll!AvrfpAPILookupCallbacksEnabled 设置为 1 来启用回调指针。结果,在恢复挂起进程后,回调指针在进程创建的用户模式部分期间执行恶意代码,从而控制执行流。

这种恶意代码在进程创建序列的早期运行,当时只有 ntdll.dll 被加载。具体来说,AvrfpAPILookupCallbackRoutine 回调在 LdrLoadDll 的初始化部分触发,如调用图所示。回调指针(ntdll!AvrfpAPILookupCallbackRoutine)和布尔变量(ntdll!AvrfpAPILookupCallbacksEnabled)在图中以浅绿色和绿色突出显示。此 LdrLoadDll 函数在用户模式下首次加载 kernelbase.dll 时执行。如果 EDR 在此点之后加载其检测措施,则可以防止它们加载其检测措施。有关详细解释及其实现方法,请参阅 EDR 预加载博客。

我们发现 EDR 预加载技术的有趣之处在于,只需通过覆盖目标 ntdll.dll 中的回调指针即可在进程创建期间获得代码执行。然而,这种代码执行受到高度限制。

4.1 代码执行限制

通过 ntdll!AvrfpAPILookupCallbackRoutine 在进程初始化期间获得的代码执行受到显著限制。这些限制是由于可用依赖项的数量有限和加载器锁同步对象施加的约束。

4.2 有限的依赖项

在 AvrfpAPILookupCallbackRoutine 回调被调用时,只有 ntdll.dll 完全加载到进程中。因此,代码执行仅限于 ntdll.dll 中的未记录 NTAPI 函数,极大地限制了可以执行的操作。无法访问其他库,如 winhttp.dll,使得执行更复杂的操作(如与命令和控制(C2)服务器通信)变得复杂。

此外,由于存在加载器锁,无法加载其他 DLL,也无法创建新线程。

4.3 加载器锁

ntdll!AvrfpAPILookupCallbackRoutine 回调在 LdrLoadDll 的初始化部分运行,在 LdrpPrepareModuleForExecution 函数下。更确切地说,回调在处理 DLL 实际初始化的 LdrpInitializeNode 内触发,如前所述。在 LdrpInitializeNode 执行期间,持有加载器锁以同步 DLL 的加载和卸载。调用图显示,此同步由 LdrpAcquireLoaderLock 管理,并由 LdrpReleaseLoaderLock 释放。

加载器锁是一个关键部分对象,防止加载其他 DLL 和创建新线程 [8]。关键部分是类似于互斥锁和信号量的同步机制,但它们设计得更高效,并用于单进程同步。有关关键部分对象的信息,请参阅 MSDN 文档 [8]。

每次函数需要访问模块数据库(PEB_LDR_DATA)时,都会获取加载器锁,涉及 DLL 加载、卸载和线程创建等任务 [9]。我们在 LdrInitializeThunk 任务的第 3 步中讨论了模块数据库。一个众所周知的访问模块数据库的函数是 GetModuleHandle,它检索 DLL 的基地址,通常用于恶意软件解析未记录的 NTAPI 函数。然而,如果在加载器锁活动期间调用此函数,例如在执行 AvrfpAPILookupCallbackRoutine 期间,会发生死锁,导致进程挂起。同样,尝试通过 LdrLoadDll 等函数加载其他 DLL 也会导致死锁。

总之,通过 AvrfpAPILookupCallbackRoutine 回调指针在进程初始化期间获得的代码执行仅限于此时已加载到进程中的模块(ntdll.dll)。此外,加载器锁防止加载其他 DLL 和创建新线程,使得执行需要访问更多模块的任务变得困难。尽管存在这些限制,EDR 预加载技术已证明,仍有足够的能力防止 EDR 加载其检测措施。

五、早期级联注入:一种新的进程注入技术

我们发现 EDR 预加载技术的有趣之处在于,只需通过覆盖目标 ntdll.dll 中的回调指针即可在进程创建期间获得代码执行。然而,正如我们所见,通过此回调获得的代码执行受到高度限制,因为加载器锁已启用,使得运行具有网络功能的完整代码(如植入物)变得不切实际。因此,我们在进程创建期间探索了新的替代技术进行进程注入。结果,我们开发了早期级联注入,这是一种从加载器锁施加的限制中产生的新型代码注入技术。

5.1 替代回调指针:g_pfnSE_DllLoaded

在我们寻找替代注入技术的过程中,我们发现了一个替代回调指针,也允许在进程创建的用户模式部分执行代码。此指针名为 g_pfnSE_DllLoaded,位于 ntdll.dll 的 .mrdata 部分。与 AvrfpAPILookupCallbackRoutine 不同,g_pfnSE_DllLoaded 似乎不在加载器锁下运行。这可以从调用图中推断出来,其中以浅蓝色和蓝色突出显示。

虽然与本文无直接相关,但了解此指针所属的内容可能会很有趣。g_pfnSE_DllLoaded 指针属于 Shim 引擎,如其名称所示,前缀 g_pfnSE 代表“全局函数指针 Shim 引擎”。Shim 引擎是 Windows 技术,负责在不修改应用程序代码的情况下应用兼容性修复,称为“shims”。它允许旧应用程序在较新版本的 Windows 上运行,通过拦截和修改 API 调用。尽管很少使用且默认禁用,但 Shim 引擎的实现仍然存在于 ntdll.dll 中,以及其指针,包括 g_pfnSE_DllLoaded。

让我们回到 g_pfnSE_DllLoaded 的关键方面。可以通过将 g_ShimsEnabled 布尔变量设置为 1 手动启用此指针,该变量位于 ntdll.dll 的 .data 部分。然而,启用此变量会启用所有与 Shim 引擎相关的指针,而不仅仅是 g_pfnSE_DllLoaded。每个指针都需要一个有效地址,如果有任何未初始化,进程将崩溃。这使得单独利用 g_pfnSE_DllLoaded 变得不切实际。

为了解决这个问题,我们专注于 g_pfnSE_DllLoaded,因为它是进程创建期间调用的第一个 Shim 引擎指针。通过针对这个指针,我们可以在其他未分配的指针执行之前执行代码,并防止它们执行。这种方法涉及将我们的 shellcode 地址分配给新进程的 g_pfnSE_DllLoaded 指针,并通过设置 g_ShimsEnabled 为 1 启用它。执行后,shellcode 立即禁用 g_ShimsEnabled,防止其余 Shim 引擎指针被调用。这种方法允许我们执行代码,而不会因未初始化的指针导致进程崩溃。

回到调用图,我们观察到 g_pfnSE_DllLoaded 在 LdrpSendPostSnapNotifications 的范围内运行,这是 LdrpPrepareModuleForExecution 的子函数。与 LdrpPrepareModuleForExecution 不同,我们观察到 g_pfnSE_DllLoaded 不在加载器锁下运行。相反,获取了一个不同的关键部分对象:LdrpDllNotificationLock。此关键部分似乎是自重入的,表明在加载其他 DLL 时不应导致死锁,尽管我们尚未验证。

尽管不在加载器锁下运行,但我们无法运行完整功能的 shellcode。这可能是由于中断了 kernelbase.dll 和 kernel32.dll 的加载过程。我们将在下一节中解决这个问题。

让我们简要回顾一下 g_pfnSE_DllLoaded 所在的内存部分,因为这是利用它的关键。g_pfnSE_DllLoaded 位于 .mrdata 部分,当进程在挂起状态下创建时,该部分是可写的。 后来,在进程初始化的用户模式部分,这部分被设为只读,如 LdrInitializeThunk 的第 2 步所述。在此步骤之后,修改其内容需要更改内存保护。

此外,g_ShimsEnabled 布尔变量位于 .data 部分,该部分在整个进程中保持可写。 这使我们能够启用或禁用 g_pfnSE_DllLoaded 指针,而无需修改内存保护。相比之下,EDR 预加载中使用的 AvrfpAPILookupCallbacksEnabled 布尔变量位于 .mrdata 部分,在 LdrInitializeThunk 的第 2 步之后需要更改内存保护。

这使得 g_pfnSE_DllLoaded 比 AvrfpAPILookupCallbackRoutine 更具优势,因为它可以在不更改内存保护的情况下禁用。因此,劫持指针所需的 shellcode 更小,只调用一次,涉及的 API 调用更少,从而降低了被检测的风险。

此外,g_pfnSE_DllLoaded 指针比 AvrfpAPILookupCallbackRoutine 触发得稍早,提供了对进程的更早控制。 类似于 EDR 预加载中利用 AvrfpAPILookupCallbackRoutine 来抢先 EDR,g_pfnSE_DllLoaded 也可以用于此目的,由于其更早的执行,可能更有效。如调用图所示,g_pfnSE_DllLoaded 在 LdrpCallInitRoutine 之前执行,该函数初始化 DLL。此时机允许我们中断实现为 DLL 的 EDR 用户模式检测措施的初始化,使其无效。例如,它可以防止 EDR 部署拦截 API 调用的钩子,显著减少 EDR 在进程中的可见性。虽然这不是本文的重点,但这为指针提供了另一个用例。

总之,我们发现了一个名为 g_pfnSE_DllLoaded 的替代指针,位于 ntdll.dll 的 .mrdata 部分。此指针可以通过位于 ntdll.dll 的 .data 部分的 g_ShimsEnabled 布尔变量启用。.mrdata 部分在进程创建的挂起状态下是可写的,而 .data 部分在整个进程中是可写的,允许我们在不更改内存保护的情况下劫持此指针。此外,g_pfnSE_DllLoaded 不在加载器锁下运行,但无法执行完全功能的 shellcode,原因未知。不过,我们怀疑这可能与关键部分对象或 kernel32.dll 和 kernelbase.dll 加载过程中的中断有关。

5.2 进程内 APC 排队

通过 g_pfnSE_DllLoaded 获得的代码执行限制让我们思考。我们意识到,在代码执行期间,我们可以调用一个执行原语,在不同阶段运行代码,摆脱限制。 我们考虑了几种执行原语,包括 NtQueueApcThread、NtCreateThread 和各种回调,如 CreateTimerQueueTimer。最终,我们发现 NtQueueApcThread 适合我们的需求并完成了工作 [6]。可以在此仓库中找到潜在回调的综合列表,作为 NtQueueApcThread 的替代方案 [7]。

通过执行原语将代码执行移动到另一个点,例如通过 NtQueueApcThread,灵感来自 Early Bird APC 注入。虽然 Early Bird APC 注入利用 APC 队列进行跨进程代码执行。

通过利用 g_pfnSE_DllLoaded 获得的代码执行,我们可以让初始线程在自身上排队一个 APC。 这使我们能够在进程创建的后期阶段过渡到不受限制的执行。我们称之为进程内 APC 排队。排队的 APC 例程指向目标内存中的恶意代码,例如植入物。

NtQueueApcThread 特别适合,因为它在 ntdll.dll 中可用,并且不受加载器锁的限制,因为它不涉及 DLL 操作或线程创建。 这意味着我们不必担心在 g_pfnSE_DllLoaded 的执行范围内调用此函数时导致死锁。

此外,NtQueueApcThread 允许我们在进程初始化阶段早期排队一个 APC,在 APC 队列清空之前。正如 LdrInitializeThunk 的第 9 步所述,最后一步之一涉及调用 NtTestAlert 以清空 APC 队列。这保证了我们排队的 APC 的执行。 此外,由于 NtTestAlert 是最后的函数之一,我们可以确定所有 DLL,包括 kernel32.dll 和 kernelbase.dll,都已完全加载,确保不会因 DLL 加载不完整而出现问题。

为了测试我们的想法,我们编写了一段 shellcode,利用 ntdll.dll 中的 NtQueueApcThread 在进程内排队一个 APC。我们称这段 shellcode 为有效载荷存根,将其写入目标内存。传递给 NtQueueApcThread 的 APC 例程指向我们写入目标内存的恶意代码地址。我们称这段恶意代码为有效载荷。因此,有效载荷存根由 g_pfnSE_DllLoaded 执行,而有效载荷通过 APC 执行。

5.3 早期级联注入技术

到目前为止,早期级联注入技术的方向应该很清楚,因为我们已经涵盖了所有关键要素和背景信息。现在是时候将所有内容结合起来,正式介绍早期级联注入!

早期级联注入的工作原理如下: 它首先在挂起状态下创建一个子进程。然后将两部分有效载荷写入其中。接下来,父进程定位 .mrdata 部分中的 g_pfnSE_DllLoaded 指针,并定位 ntdll.dll 的 .data 部分中的 g_ShimsEnabled 布尔变量。然后,它将第一部分有效载荷,即有效载荷存根的地址分配给新进程的 g_pfnSE_DllLoaded 指针,并通过将 g_ShimsEnabled 设置为 1 来启用它。最后,它恢复挂起的进程。结果,新进程的初始线程执行有效载荷存根。此有效载荷立即通过将 g_ShimsEnabled 设置为 0 来禁用它,防止其余 Shim 引擎相关指针执行。然后,有效载荷存根使用 NtQueueApcThread 在自身上排队第二部分有效载荷,即初始线程。此 APC 由 NtTestAlert 函数在 Windows 映像加载器的末尾触发。结果,主有效载荷执行。主有效载荷可以是包含攻击者希望在目标系统上运行的主要功能的植入物。

在图 2 中,早期级联注入的流程如上所述。

【图 2】早期级联注入流程

早期级联注入是一种新颖的注入技术,可以作为 Early Bird APC 注入的替代方案。与 Early Bird APC 注入相比,早期级联注入的主要优势在于它不涉及远程执行原语(跨进程 APC 排队)。此外,早期级联注入与 Early Bird APC 注入不同,目前未被记录,并通过不跨进程排队 APC 打破了传统的代码注入模式。我们在多个 EDR 上进行了测试,包括顶级 EDR,未被检测到。

关键特性

无远程执行原语: 早期级联注入避免了远程执行原语,如 QueueUserAPC。就像无线程注入方法一样,它利用指针来执行有效载荷,避免了远程执行原语的需求。

最小的远程进程交互: 早期级联注入仅涉及远程内存分配、保护和写入。

可写的 .mrdata 和 .data: .mrdata 部分在挂起状态下是可写的,允许在不更改内存保护的情况下进行修改。此外,.data 在整个进程中都是可写的,允许在不更改内存保护的情况下启用/禁用 g_pfnSE_DllLoaded。

新颖技术: 由于早期级联注入的新颖方法,其调用模式不太可能被安全产品识别,从而降低了被检测的风险。

未记录的回调: 早期级联注入依赖于未记录的指针 g_pfnSE_DllLoaded,该指针可能会随着 Windows 更新而改变,可能影响其可靠性。

六、EDR 检测措施加载机制和时机

在最后一节中,我们探讨了 EDR 在进程创建期间加载其用户模式检测措施(如钩子)的方式和时机。了解这些措施的时机对于开发预防和规避它们的策略至关重要。预防意味着在这些检测措施到位之前获得对进程的控制。为了保密,我们不会提及具体的 EDR 名称。

为了更清楚地理解用户模式检测机制,我们将简要讨论钩子,使用它们作为示例。此外,用户模式钩子是 EDR 用于检测恶意活动的关键检测措施之一。特别是,随着微软逐步限制内核访问,这迫使 EDR 将检测措施转移到用户模式 [5]。微软确实提供了像 Windows 事件跟踪 (ETW) 这样的替代方案,但这些尚未被广泛采用。这在不久的将来可能会改变。

钩子允许 EDR 通过拦截进程内的 API 调用来实时监控进程。通过防止这些钩子加载,攻击者可以显著减少 EDR 的可见性,从而增加恶意软件规避检测的机会。一种有效的方法是通过在钩子完全加载并生效之前采取行动来避免钩子。通常,EDR 在进程创建的用户模式部分通过一个钩子 DLL 来放置钩子。在接下来的部分中,我们将详细解释其工作原理。

在深入技术细节之前,了解内核驱动在 EDR 中的作用是至关重要的。该驱动使 EDR 能够注册通知回调例程,以接收有关系统事件(如进程创建或终止、映像加载、注册表更改和系统关闭请求)的警报。这些回调收集系统信息,EDR 可能会基于此采取未来的行动。例如,在收到进程创建通知后,EDR 可以将其钩子 DLL 注入新进程以进行监控。

进程通知回调存储在内核的 nt!PspCreateProcessNotifyRoutine 数组中,该数组包含所有注册的回调。当创建新进程时,内核函数 nt!PspCallProcessNotifyRoutines 会遍历此数组,调用每个回调。有关 EDR 组件及其与 Windows 交互的更多信息,我们推荐阅读 Matt Hand 的《Evading EDR》一书。

顺便提一下,有一些规避工具可以注销内核回调,以防止 EDR 加载额外的安全措施 [10]。然而,这种方法需要访问内核,通常通过利用易受攻击的内核驱动来实现。通过修改内核的通知回调,这些工具可以阻止 EDR 加载其用户模式检测措施。然而,这种技术所需的内核访问,使其成为一种复杂的规避方法。

回到主要观点,EDR 使用进程创建通知作为触发器,将用户模式检测措施加载到新创建的进程中。我们分析了几个 EDR,以了解这些检测措施的加载方式。根据我们的发现,我们解释了 EDR 注入其用户模式检测模块的一般方法。

我们观察到,当新创建的进程从挂起状态恢复时,EDR 在从内核到用户模式过渡之前修改 ntdll.dll(LdrInitializeThunk)。具体来说,EDR 将 shellcode 注入进程内存,其中包含加载 EDR 钩子 DLL 的逻辑。此外,它们在 LdrInitializeThunk 中放置一个钩子,将代码执行重定向到注入的 shellcode。在我们对各种 EDR 的分析中,我们发现钩子特别放置在 LdrLoadDll、LdrpLoadDll 或 NtContinue 中的 LdrInitializeThunk。图 3 重新审视了调用图,并突出了这些函数。请注意,EDR 也使用此机制加载其检测措施,用于未在挂起状态下创建的进程。

【图 3】红色箭头指向 EDR 钩子加载其用户模式检测措施的函数

【例如】,图 4 显示了 LdrLoadDll 上的钩子。LdrLoadDll 的初始字节被替换为一个跳转指令,指向 EDR 的 shellcode。这个被钩住的 LdrLoadDll 作为 LdrInitializeThunk 的从属函数被调用。当 LdrLoadDll 执行时,执行流被重定向到注入的 shellcode。

这个 shellcode 负责加载 EDR 的检测措施。图 5 描绘了加载 EDR 钩子 DLL 的 shellcode 的调用栈。在调用栈中,我们可以看到根函数是未备份的,这意味着它不是合法模块的一部分,这表明它已被注入进程。

【图 4】LdrLoadDll 的初始字节已被替换为跳转指令,指向 EDR 的 shellcode

【图 5】加载 EDR 钩子 DLL 的 shellcode 的调用栈

注入的 shellcode 将钩子 DLL 的路径和名称写入 rcx 和 r9(遵循 x64 快速调用约定),然后调用 LdrLoadDll。一旦钩子 DLL 被加载,其入口点(例如 DllMain)被执行,负责在关键函数上放置钩子。在 LdrLoad 完成后,shellcode 移除内联钩子并恢复进程的正常执行流。从此时起,EDR 可以实时拦截 API 调用并监控进程。

在调用图中,我们可以观察到 LdrLoadDll、LdrpLoadDll 和 NtContinue 首次执行的时间点。根据具体的 EDR,在这些点之一,EDR 的检测措施(例如钩子 DLL)被加载。

对于钩住 NtContinue 的 EDR,像 Early Bird APC 和早期级联注入这样的技术可以预先加载 EDR 的检测措施。这意味着恶意代码(例如植入物)在检测措施加载之前运行。在调用图中,我们可以看到 NtTestAlert 在 NtContinue 之前执行。由于 NtTestAlert 清空了 APC 队列,它确保了恶意代码在 EDR 的检测措施激活之前运行。

对于钩住 LdrLoadDll 和 LdrpLoadDll 的 EDR,EDR 在进程的早期阶段获得控制,在加载 kernel32.dll 时,这比 g_pfnSE_DllLoaded 早。这使得 EDR 在我们之前获得对进程的控制。然而,正如我们在调用图中所看到的,g_pfnSE_DllLoaded 在 EDR 初始化之前为我们提供了控制。这意味着,尽管 EDR 首先获得控制,我们仍然可以在 DLL 初始化之前获得控制,阻止 EDR 加载其检测措施。

我们还观察到,大多数 EDR 通过 shellcode 最初加载 kernel32.dll 和 kernelbase.dll,遵循正常的执行流。之后,它们通过 LdrLoadDll 加载其钩子 DLL。请记住,g_pfnSE_DllLoaded 在 LdrLoadDLL 的初始化部分执行,在这种情况下是针对 kernelbase.dll。这远在 EDR 的检测措施被 shellcode 加载之前。在理论上,在这个阶段,我们可以移除 LdrLoadDll 上的钩子,恢复加载 kernel32.dll 的原始代码路径,并继续执行,绕过 EDR 的加载过程。

可能有许多方法可以使用本文讨论的回调指针来防止用户模式 EDR 检测措施。我们提出了一种潜在的方法,可以通过利用 g_pfnSE_DllLoaded 回调指针将其集成到早期级联注入中。这将允许通过早期级联注入注入的植入物更隐蔽地运行,进一步规避 EDR 检测。

结论

在这篇博客中,我们探讨了 Windows 中进程创建的方式,重点关注进程创建的用户模式部分。我们展示了一个调用图,概述了进程创建期间的关键事件。然后,我们研究了 Early Bird APC 注入的工作原理及其与用户模式部分的交互,特别是排队的 APC 何时执行。之后,我们讨论了 EDR 预加载,展示了如何通过覆盖指针在进程创建期间实现代码执行。这引导我们进一步调查并发现了一个新的指针。然而,通过它无法执行完全功能的代码。通过将 Early Bird APC 的 APC 排队元素与新指针结合,受 EDR 预加载的启发,我们开发并解释了早期级联注入。最后,我们强调了这种技术的关键特性。希望您发现调用图和我们一样具有信息性——提供了进程创建的概述,揭示了 EDR 安全措施的时机,并展示了早期级联注入如何与进程创建交互。
参考资料

[1] Bypassing EDRs With EDR-Preloading – Marcus Hutchins
[2] Process Creation Flags – MSDN
[3] Windows 10 Parallel Loading Breakdown – BlackBerry
[4] New ‘Early Bird’ Code Injection Technique Discovered – Cyberbit
[5] Microsoft is building new Windows security features to prevent another CrowdStrike incident – The Verge
[6] NtQueueApcThread – NTAPI Undocumented Functions – NTInternals
[7] AlternativeShellcodeExec – aahmad097
[8] Critical Section Objects – MSDN
[9] What is Loader Lock? – Elliot Killick
[10] Removing Kernel Callbacks Using Signed Drivers – br-sn
[11] Demystifying Shims – or – Using the App Compat Toolkit to make your old stuff work with your new stuff – MSDN

原创 Miggelenbrink securitainment

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/871530.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

CODEFORCE DIV2 NO.996(好社畜的场次名)

这一次的博客其实早就应该发布了,但是当时急着回家睡觉,于是就直接把博客的编辑页面给关闭了,于是没有保存,完成了3/4的博客就这样没有了,对,所以这件事启示了我们写完博客一定要保存好草稿,不然就是唐完了。问就是唐龙 首先是这场比赛的评价,当时真的是犯蠢了,感觉是…

【Atcoder训练记录】AtCoder Beginner Contest 389

训练情况赛后反思 赛后VP的,C题忘记vector里面erase复杂度是 O(n) 的了,导致TLE了两发,换成双端deque就过了 A题 取字符串第一位和第三位取int相乘 #include <bits/stdc++.h> // #define int long long #define endl \nusing namespace std;void solve(){string s; ci…

一图理解RAG与Agentic RAG的区别

RAG 是一种结合了信息检索和生成模型的自然语言处理技术框架,能够提高 AI 系统在回答自然语言问题时准确性和可靠性,但是传统 RAG 还有不少问题,比如: 它检索一次生成一次。如果上下文不足,无法动态搜索更多信息。 它无法对复杂查询进行推理。 系统无法根据具体问题调整策…

常用的9款工业调试工具

modbus调试工具 这个工具是用来调试modbus通讯协议报文的。分二个一个是模拟modbus协议一个是监听modbus通讯协议。 poll是监听工具slave是模拟工具。大家在我提供的安装包里都有可自行选择,怎么使用可以看往期间文章串口调试工具 串口调试工具,需要设备对应的波特率、停止位…

C#实战附俄罗斯方块实战

C#实战 ArrayList using System; using System.Collections; using System.Security.Principal; namespace ArrayList数组; class Program {static void Main(string[] args){#region 本质/* ArrayList是一个C#封装好的类本质是一个object类型的数组ArrayList,*/#endregion#reg…

在线图片转为excel工具

在线图片转为excel工具,无需登录,无需成本,用完就走。包括中文和英文版本。官网地址:https://img2excel.openai2025.com效果:

在线图片压缩工具

在线图片压缩工具,无需登录,无需成本,用完就走。包括中文和英文版本。官网地址: https://compress.openai2025.com/ 效果:

在线图片水印处理工具

在线图片水印处理工具,无需登录,无需费用,用完就走。包括中文和英文版本https://watermark.openai2025.com/

在线图片像素颜色拾取工具

在线图片像素颜色拾取工具,非常方便的一个工具,无需登录,用完就走。包括中文和英文版本。https://getcolor.openai2025.com

在线base64转码工具

在线base64转码工具,无需登录,无需费用,用完就走。官网地址:https://base64.openai2025.com效果:

在线json格式化工具

在线json格式化工具,包括中文和英文版本,无需登录,无需费用,用完就走。官网地址: https://json.openai2025.com效果如下:

通过雨云每天最低低保可以挣到5元的方法

最近使用雨云的服务器,发现他们家有个活动。就是发布任何类型的视频,只要提到他们家的服务器,就可以申请领取最低5元最高200元的奖励,如果有做视频的兄弟,每天随便做个简单的视频,最起码领取一个低保没问题吧!一天5元钱一个月150 一年大概1800块钱,服务器 域名钱都有了…