介绍
在这篇博文中,我们介绍了一种名为早期级联注入的新型进程注入技术,探索了 Windows 进程创建,并确定了多个端点检测和响应系统 (EDR) 如何初始化其进程内检测功能。这种新的早期级联注入技术针对进程创建的用户模式部分,并将著名的 Early Bird APC 注入技术的元素与 Marcus Hutchins [1]最近发布的 EDR-Preloading 技术相结合。与 Early Bird APC 注入不同,这种新技术避免了排队跨进程异步过程调用 (APC),同时将远程进程交互降至最低。这使得早期级联注入成为一种隐秘的进程注入技术,可有效对抗顶级 EDR,同时避免被检测到。
为了深入了解早期级联注入的内部原理,本博文还介绍了用户模式进程创建流程的时间线。本概述说明了早期级联注入的运作方式,并指出了它干预进程创建的确切时刻。此外,我们将其与 EDR 用户模式检测措施的初始化时间进行了比较。
现在,让我们深入了解 Windows 进程创建、Early Bird APC 注入和 EDR 预加载的细节。一旦我们对这些主题有了扎实的了解,我们就可以继续探索早期级联注入。
了解 Windows 进程创建
进程创建 API
在 Windows 中,有各种 API 可用于创建进程,例如CreateProcess、CreateProcessAsUser和CreateProcessWithLogon,如图 1 所示。最终,所有这些函数都会调用NtCreateUserProcess中的NAPI ntdll.dll。此函数负责通过将控制权切换到内核来启动进程创建,在内核中执行同名函数NtCreateUserProcess。
这些函数中的每一个都包含dwCreationFlags参数,用于控制进程的创建方式。在本文中,我们将遇到标志CREATE_SUSPENDED,它指示内核在挂起状态下创建新进程的初始线程[2]。线程将保持挂起状态,直到ResumeThread调用该函数。
显然,这些函数还有一个参数,指定要为其创建进程的应用程序映像文件的路径。有关这些 API 的其他参数和标志,请参阅 MSDN [2]。https://learn.microsoft.com/en-us/windows/win32/procthread/process-creation-flags
图 1:进程创建函数(来源:Windows Internals,第 1 部分)
内核模式和用户模式进程创建
进程创建分为两个部分:内核模式和用户模式。首先是内核模式部分,由 启动NtCreateUserProcess。在内核模式下创建进程的上下文和环境后,进程的初始线程将在用户模式下完成进程创建。
内核模式部分负责打开指定应用程序的映像文件并将其映射到内存中。然后,它创建特定于进程和特定于线程的对象,将本机库映射ntdll.dll到进程中,然后创建进程的初始线程。如果CREATE_SUSPENDED指定了标志,则此线程将以挂起状态创建,等待恢复,然后控制权将返回到用户模式以完成进程创建的其余部分。
模块ntdll.dll是第一个加载到进程中的 DLL,并且是唯一一个在内核模式下加载的 DLL,所有其他模块均在用户模式下加载。此外,ntdll.dll还包括导出函数LdrInitializeThunk,该函数在应用程序的主入口点运行之前处理进程创建的用户模式部分。此函数也称为图像加载器,与其相关的函数ntdll.dll以 为前缀Ldr。
返回到新创建的线程:恢复此挂起线程后,它将开始执行LdrInitializeThunk进程创建的用户模式部分。之后,新进程将完全初始化并准备好运行应用程序。然后,初始线程开始执行应用程序的主入口点。
请注意,使用该CREATE_SUSPENDED标志会在初始线程切换到用户模式运行之前暂停进程创建LdrInitializeThunk。这特别有趣,因为用户模式恶意软件可以在此时干扰进程创建。因此,让我们仔细看看内部发生了什么LdrInitializeThunk。
用户模式进程创建:LdrInitializeThunk
LdrInitializeThunk是第一个在用户模式下执行的函数,标志着恶意软件和 EDR 可以干预进程的初始点。我们稍后将探讨 Early Bird APC 注入、EDR 预加载和 Early Cascade Injection 等技术如何与 交互LdrInitializeThunk。现在,让我们深入研究该函数的细节。
LdrInitializeThunk是一个负责进程创建的用户模式部分的复杂函数。它处理许多进程初始化任务,这些任务在《Windows Internals,第 1 部分》一书中列出并简要描述。但是,该书并未介绍其中哪些下属函数LdrInitializeThunk负责这些任务。因此,为了更深入地了解LdrInitializeThunk及其下属函数,我们使用 x64dbg 和 IDA Pro 对其进行了分析。
基于此分析,我们创建了一个调用图,概述了进程创建过程中用户模式部分的事件顺序。此时间线包括与本文相关的函数,因此省略了一些任务和相关函数LdrInitializeThunk。此外,请注意,此调用图反映了我们的解释,可能并不完全准确。
的调用图LdrInitializeThunk如下所示,后面是其中可识别的任务的描述。关键函数以颜色突出显示。
调用图LdrInitializeThunk:
此调用图说明了执行的以下任务LdrInitializeThunk:
1.在进程环境块中初始化加载程序锁(PEB)。
-
是PEB存储进程上下文和环境信息的数据结构。Loader Lock 将在后面讨论。
2.设置可变只读堆部分(.mrdata)。
3.创建第一个加载器数据表条目 ( LDR_DATA_TABLE_ENTRY)并将其插入ntdll.dll到模块数据库 ( PEB_LDR_DATA) 中。
-
模块数据库存储在 中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 Engine 并解析 Shim Database。Shim Engine 默认关闭。
-
Shim Engine 无需修改代码即可对应用程序应用兼容性修复程序(shim)。它会拦截并修改 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)。此外,我们观察到通过加载 DLLLdrLoadDll包含两个步骤:映射和初始化。
LdrLoadDll工作原理
加载依赖项是 的一个主要组件LdrInitializeThunk,这可能就是它被称为镜像加载器的原因。此外,EDR-Preloading 和 Early Cascade Injection 技术专门LdrLoadDll在进程创建期间干预此功能。因此,我们还简要介绍了如何LdrLoadDll加载依赖项。之后,我们将开始探索有趣的东西:进程注入。
本节之后要记住的最重要的方面是LdrLoadDll加载 DLL 分为两个步骤:首先,它将DLL 的映像文件映射LdrpFindOrPrepareLoadingModule到内存中;其次,它通过初始化DLL LdrpPrepareModuleForExecution。如果 DLL 具有递归依赖关系,则首先将它们映射到内存中,然后才初始化每个模块。初始化按相反顺序执行,以遵循正确的依赖顺序。
现在,让我们深入研究一下细节,并了解加载的过程kernel32.dll,这是在进程创建期间加载的第一个模块。试着在调用图中跟踪。我们从加载的LdrInitializeThunk调用开始。调用第一步:映射到内存中。实际的映射最终由嵌套函数执行。之后,检查依赖项,并且由于从导入函数,因此映射到内存中。LdrpLoadDllkernel32.dllLdrpLoadDllntdll!LdrpFindOrPrepareLoadingModulekernel32.dllNtMapViewOfSectionLdrpMapAndSnapDependencykernel32.dllkernelbase.dllLdrpLoadDependentModulekernelbase.dll
一旦两个模块被映射到内存中, 的第二部分LdrLoadDll将初始化 DLL,由 执行LdrpPrepareModuleForExecution。此函数调用LdrpCondenseGraph来创建依赖关系图,该图存储了依赖关系必须初始化的顺序。接下来,LdrpInitializeGraphRecurse处理此图,并对图中的每个模块LdrpInitializeNode调用 。图中的第一个节点是kernelbase.dll通过LdrpInitializeNode进行初始化LdrpCallInitRoutine的。此函数调用 的入口点kernelbase.dll。之后,执行相同的操作来初始化kernel32.dll;此后,LdrLoadDll已完成加载kernel32.dll。
Early Bird APC 注射剂
现在我们对进程创建有了大致的了解,让我们仔细看看 Cyberbit 在 2018 年发现的 Early Bird APC 注入技术[4]。这是一种众所周知且有效的进程注入方法,涉及在执行进程的主入口点之前注入代码。如果在 APC 执行之前未加载这些措施(包括钩子),则在进程早期注入此方法可能会逃避 EDR 检测措施。
Early Bird APC 注射技术的工作原理如下:
-
创建一个处于暂停状态的目标进程(例如CreateProcess);
-
在目标进程中分配可写内存(例如VirtualAllocEx);
-
向分配的内存中写入恶意代码(例如WriteProcessMemory);
-
将 APC 排队到远程目标进程,该 APC 指向恶意代码(例如QueueUserAPC);
-
恢复目标进程,恢复后执行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 注入的有力指标。稍后,我们将看到 Early Cascade Injection 如何在没有跨进程排队的情况下执行注入,因此无法被我们测试的 EDR 检测到。
EDR 预加载
早期级联注入结合了 EDR-Preloading 的元素。因此,让我们简要介绍一下 EDR-Preloading,这是 Marcus Hutchins 最近在博客中介绍的,他因阻止 WannaCry [1]而闻名。他的博客启发了我在这方面的研究,谢谢!
EDR-Preloading 旨在防止 EDR 在进程创建期间加载其用户模式检测措施。例如,它可以防止 EDR 初始化其 Hooking DLL,这会大大降低 EDR 在进程内的可见性,因为 EDR 无法拦截 API 调用。随着 Microsoft 逐渐限制第三方对内核的访问,将 EDR 检测措施从内核模式强制为用户模式,此类技术变得越来越重要。据推测,由于 Crowdstrike 事件导致 850 万台 Windows 系统因内核级软件更新错误而瘫痪,内核限制将进一步推行[5]。
EDR-Preloading 的工作原理:首先创建一个处于挂起状态的进程,然后劫持ntdll!AvrfpAPILookupCallbackRoutine中的回调指针ntdll.dll。劫持涉及将恶意代码的起始地址分配给回调指针,并通过设置为 来启用回调指针ntdll!AvrfpAPILookupCallbacksEnabled。1因此,在恢复挂起的进程后,回调指针会在进程创建的用户模式部分执行。一旦调用,恶意代码就会运行,从而在进程创建期间控制执行流。
此恶意代码在进程创建序列中非常早期的运行,此时仅ntdll.dll加载了 。具体来说,回调AvrfpAPILookupCallbackRoutine是在 的初始化部分触发的LdrLoadDll,如调用图所示。回调指针(ntdll!AvrfpAPILookupCallbackRoutine)和布尔变量(ntdll!AvrfpAPILookupCallbacksEnabled)在图中以浅绿色和绿色突出显示。此LdrLoadDll函数在 的初始化期间首次执行kernelbase.dll,这是第一个在用户模式下加载的 DLL。如果 EDR 在此之后加载其检测措施,则可以阻止它们加载其检测措施。有关此操作及其实现方法的详细说明,请参阅 EDR-Preloading 博客。
ntdll.dll我们发现 EDR-Preloading 技术的一个有趣之处在于,只需在进程创建期间覆盖目标中的回调指针即可实现代码执行。但是,这种代码执行受到严格限制。
代码执行限制
进程初始化期间获得的代码执行ntdll!AvrfpAPILookupCallbackRoutine受到很大限制。这些限制是由可用依赖项数量有限以及Loader Lock同步对象施加的限制造成的。
有限的依赖性
在调用AvrfpAPILookupCallbackRoutine回调时,只有ntdll.dll完全加载到进程中。因此,代码执行仅限于 中未记录的 NTAPI 函数ntdll.dll,这大大限制了可以执行的操作。无法访问其他库(例如winhttp.dll)会使执行更复杂的操作(例如与命令和控制 (C2) 服务器通信)变得复杂。
而且由于Loader Lock的存在,所以无法加载任何额外的DLL,也无法创建新的线程。
装载机锁
回调ntdll!AvrfpAPILookupCallbackRoutine在 的初始化部分期间LdrLoadDll在函数 下运行LdrpPrepareModuleForExecution。更准确地说,回调在 内触发LdrpInitializeNode,它处理 DLL 的实际初始化,如前所述。在 的执行期间LdrpInitializeNode,将持有 Loader Lock 以同步 DLL 的加载和卸载。调用图显示此同步由 管理LdrpAcquireLoaderLock和释放LdrpReleaseLoaderLock。
加载器锁是一个临界区对象,用于阻止加载其他 DLL 和创建新线程[8]。临界区是一种类似于互斥锁和信号量的同步机制,但它们的设计效率更高,并且适用于单进程同步。有关临界区对象的信息,请参阅 MSDN 文档[8]。
每次函数需要访问模块数据库(PEB_LDR_DATA)时,都会获取加载器锁,该数据库涉及 DLL 加载、卸载和线程创建等任务[9]。我们之前在 的LdrInitializeThunk任务步骤 3 中讨论了模块数据库。访问模块数据库的一个著名函数是GetModuleHandle,它检索 DLL 的基址,恶意软件经常使用它来解析未记录的 NTAPI 函数。但是,如果在加载器锁处于活动状态时调用此函数(例如在 的执行期间)AvrfpAPILookupCallbackRoutine,则会发生死锁,导致进程挂起。同样,尝试通过 等函数加载其他 DLL 也会LdrLoadDll导致死锁。
总之,AvrfpAPILookupCallbackRoutine在进程初始化期间通过回调指针执行的代码仅限于当时已加载到进程中的模块(ntdll.dll)。此外,加载器锁可防止加载其他 DLL 和创建新线程,从而难以执行需要访问更多模块的任务。尽管存在这些限制,但 EDR-Preloading 技术已证明它具有足够的能力来阻止 EDR 加载其检测措施。
早期级联注入:一种新的工艺注入技术
我们发现 EDR-Preloading 技术的一个有趣之处在于,您只需在进程创建期间覆盖目标中的回调指针即可获得代码执行ntdll.dll。但是,正如我们所见,由于启用了加载程序锁,通过此回调获得的代码执行受到严格限制,因此无法运行功能齐全的代码(例如具有网络功能的植入程序)。因此,我们在进程创建期间探索了用于进程注入的新颖和替代技术。结果,我们开发了早期级联注入,这是一种从加载程序锁施加的限制中产生的新颖代码注入技术。
另一个回调指针:g_pfnSE_DllLoaded
在寻找替代注入技术的过程中,我们发现了一个替代回调指针,它也允许在进程创建的用户模式部分执行代码。这个指针名为g_pfnSE_DllLoaded,位于.mrdata的部分ntdll.dll。与不同AvrfpAPILookupCallbackRoutine,g_pfnSE_DllLoaded似乎不在加载程序锁下运行。这可以从调用图中推断出来,其中它以浅蓝色和蓝色突出显示。
虽然与本博客没有直接关系,但了解这个指针属于什么可能会很有趣。指针g_pfnSE_DllLoaded属于 Shim Engine,正如其名称所示,前缀g_pfnSE代表“全局函数指针 Shim Engine”。Shim Engine 是一种 Windows 技术,负责应用兼容性修复程序(称为“垫片”),而无需修改应用程序代码。它允许旧应用程序在较新版本的 Windows 上运行,方法是拦截和修改 API 调用。虽然很少使用且默认情况下被禁用,但 Shim Engine 的实现仍然存在于 中ntdll.dll,以及它的指针,包括g_pfnSE_DllLoaded。
让我们回到 的关键方面g_pfnSE_DllLoaded。可以通过将中的布尔变量设置为 来手动启用指针g_ShimsEnabled1。但是,启用此变量会启用所有与 Shim Engine 相关的指针,而不仅仅是。这些指针中的每一个都需要一个有效的地址,如果任何指针仍未初始化,则进程将崩溃。这使得在没有解决其他指针的情况下单独利用是不切实际的。.datantdll.dllg_pfnSE_DllLoadedg_pfnSE_DllLoaded
为了克服这个问题,我们特别关注了g_pfnSE_DllLoaded,因为它是进程创建期间调用的第一个 Shim Engine 指针。通过瞄准这个指针,我们可以在任何其他未分配的指针之前执行代码并阻止它们执行。此方法涉及将我们的 shellcode 的地址分配给g_pfnSE_DllLoaded并启用g_ShimsEnabled以激活它。执行后,shellcode 立即禁用g_ShimsEnabled,防止调用其余的 Shim Engine 指针。这种方法允许我们执行代码,而不会因未初始化的指针而导致进程崩溃。
回到调用图,我们观察到g_pfnSE_DllLoaded在的范围内运行LdrpSendPostSnapNotifications,它是的下属函数LdrpPrepareModuleForExecution。与不同LdrpPrepareModuleForExecution,我们观察到g_pfnSE_DllLoaded不在加载器锁下运行。相反,获取了不同的临界区对象:LdrpDllNotificationLock。此临界区似乎是自重入的,这表明它在加载其他 DLL 时不应导致死锁,尽管我们尚未验证。
尽管没有在 Loader Lock 下运行,我们还是无法运行功能齐全的 shellcode。这可能是由于中断了 kernelbase.dll 和 kernel32.dll 的加载过程。我们将在下一节中解决这个问题。
让我们简要回顾一下 所在的内存部分g_pfnSE_DllLoaded,因为这对于利用它至关重要。g_pfnSE_DllLoaded位于.mrdata部分,当进程在挂起状态下创建时,该部分是可写的。稍后,在进程初始化的用户模式部分,此部分将变为只读,如 的步骤 2 中所述LdrInitializeThunk。在此步骤之后,修改其内容需要更改内存保护。
此外,布尔g_ShimsEnabled值位于.data部分中,该部分在整个过程中保持可写状态。这使我们能够启用或禁用g_pfnSE_DllLoaded指针而无需修改内存保护。相比之下,AvrfpAPILookupCallbacksEnabledEDR-Preloading 中使用的布尔值位于.mrdata部分中,并且要求在第 2 步之后更改内存保护LdrInitializeThunk。
这比g_pfnSE_DllLoaded更可取AvrfpAPILookupCallbackRoutine,因为它可以在不改变内存保护的情况下被禁用。因此,劫持指针所需的 shellcode 更小,只调用一次,涉及的 API 调用更少,因此降低了被检测到的风险。
此外,指针g_pfnSE_DllLoaded的触发时间比 稍早AvrfpAPILookupCallbackRoutine,从而可以更早地控制进程。类似于AvrfpAPILookupCallbackRoutine在 EDR-Preloading 中利用 来抢占 EDR 的方式,g_pfnSE_DllLoaded也可以用于此目的,由于其执行时间较早,因此可能具有更高的效率。如调用图所示,g_pfnSE_DllLoaded在 之前执行LdrpCallInitRoutine,这会初始化一个 DLL。这个时间允许我们破坏以 DLL 形式实现的 EDR 用户模式检测措施的初始化,从而使其无效。例如,它可以阻止 EDR 部署拦截 API 调用的钩子,从而显著降低 EDR 在进程中的可见性。虽然这不是本博客的重点,但这为指针提供了另一个用例。
总之,我们确定了g_pfnSE_DllLoaded位于.mrdata部分中的名为 的替代指针ntdll.dll。可以通过g_ShimsEnabled位于.data部分的布尔值启用此指针ntdll.dll。.mrdata部分在进程创建的挂起状态下可写,部分.data在整个进程中都可写,允许使用劫持此指针而不更改内存保护。此外,不在加载程序锁下运行,但由于未知原因,执行功能齐全的 shellcode 并非易事。不过,我们怀疑这可能与关键部分对象有关,或者是因为和加载过程g_pfnSE_DllLoaded中的中断。kernel32.dllkernelbase.dll
进程内 APC 排队
通过 执行代码的局限性g_pfnSE_DllLoaded引起了我们的思考。然后我们意识到,在代码执行期间,我们可以调用执行原语来在不同阶段运行代码,从而摆脱 的限制。我们考虑了几个执行原语,包括NtQueueApcThread和NtCreateThread各种回调,例如CreateTimerQueueTimer。最终,我们发现NtQueueApcThread适合我们的需求并完成了工作[6] 。可以在此存储库[7]NtQueueApcThread中找到可替代 的潜在回调的完整列表。
使用执行原语将代码执行移至另一点(例如通过NtQueueApcThread)的灵感来自 Early Bird APC Injection。尽管 Early Bird APC Injection 利用 APC 队列进行跨进程代码执行。
通过利用通过 获得的代码执行g_pfnSE_DllLoaded,我们可以让初始线程在其自身上排队 APC。这使我们能够在进程创建后期过渡到不受限制的执行。我们将此称为进程内 APC 排队。排队的 APC 例程指向目标内存中的恶意代码,例如植入物。
NtQueueApcThread特别合适,因为它在 中可用ntdll.dll,并且不受加载程序锁的约束,因为它不涉及 DLL 操作或线程创建。这意味着我们不必担心在 的执行范围内调用此函数会导致死锁g_pfnSE_DllLoaded。
此外,NtQueueApcThread允许我们在进程初始化阶段早期(APC 队列清空之前)对 APC 进行排队。如 的步骤 9 中所述LdrInitializeThunk,最后一步涉及调用NtTestAlert来清除 APC 队列。这保证了排队的 APC 的执行。此外,由于NtTestAlert是最后几个函数之一,我们可以确定所有 DLL(包括kernel32.dll和kernelbase.dll)都已完全加载,从而确保不会因 DLL 加载不完整而出现问题。
为了测试我们的想法,我们编写了一段利用NtQueueApcThread来ntdll.dll排队进程内 APC 的 shellcode。我们将此 shellcode 称为有效载荷存根,我们将其放入目标内存中。传递给 的 APC 例程指向NtQueueApcThread我们已写入目标内存的恶意代码的地址。我们将此恶意代码称为有效载荷。因此,有效载荷存根由 执行g_pfnSE_DllLoaded,而有效载荷则通过 APC 执行。
早期级联注入技术
到目前为止,早期级联注入技术的方向应该很明确了,因为我们已经介绍了所有关键要素和背景信息。现在是时候将所有内容整合在一起并正式介绍早期级联注入了!
早期级联注入的工作原理如下:首先创建一个处于挂起状态的子进程。然后,它将一个由两部分组成的有效载荷写入其中。接下来,父进程g_pfnSE_DllLoaded在部分中定位指针,并在部分中.mrdata定位g_ShimsEnabled布尔变量。接下来,它将第一个有效载荷部分(有效载荷存根)的地址分配给新进程的这个指针,并通过将其设置为来启用它。最后,它恢复挂起的进程。结果,新进程的初始线程执行有效载荷存根。此有效载荷立即通过将设置为来禁用,从而阻止其余与 Shim Engine 相关的指针执行。然后,有效载荷存根使用将有效载荷的第二部分作为 APC 排队到自身(即初始线程)上。此 APC 由函数在 Windows 映像加载器末尾附近触发。结果,主有效载荷执行。主有效载荷可能是一个植入物,其中包含攻击者想要在目标系统上运行的主要功能。.datantdll.dllg_pfnSE_DllLoadedg_ShimsEnabled1g_ShimsEnabled0NtQueueApcThreadNtTestAlert
在图 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 撰写的《逃避 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被替换为指向注入的 shellcode 的跳转指令。这个钩子版本的LdrLoadDll被称为 的下属函数LdrInitializeThunk。执行时LdrLoadDll,执行流被重定向到注入的 shellcode。
此 shellcode 负责加载 EDR 的检测措施。图 5 描述了加载 EDR 挂钩 DLL 的 shellcode 的调用堆栈。在调用堆栈中,我们可以看到根函数未备份,这意味着它不是合法模块的一部分,这表明它已被注入到进程中。
图 4:LdrLoadDll的初始字节已被替换为跳转到 EDR 的 shellcode 的指令
图 5:EDR shellcode 的调用堆栈,加载其挂钩 DLL
注入的 shellcode 将挂钩 DLL 的路径和名称写入rcx和r9(遵循 x64 fastcall 约定),然后调用LdrLoadDll。一旦加载了挂钩 DLL,DllMain就会执行其入口点(例如),负责启动关键函数上的挂钩。LdrLoad完成后,shellcode 会删除内联挂钩并恢复进程的正常执行流程。从此时起,EDR 可以实时拦截 API 调用并监视进程。
在调用图中,我们可以观察到第一次LdrLoadDll、LdrpLoadDll和NtContinue执行。在其中一个点,根据特定的 EDR,EDR 的检测措施(例如挂钩 DLL)会被加载。
对于 EDR 挂钩NtContinue,Early Bird APC 和 Early Cascade Injection 等技术会抢占 EDR 的检测措施。这意味着恶意代码(例如植入程序)在检测措施加载之前运行。在调用图中,我们可以看到NtTestAlert在之前执行NtContinue。由于NtTestAlert清空了 APC 队列,因此它确保恶意代码在 EDR 的检测措施激活之前运行。
对于 EDR 挂钩LdrLoadDll,LdrpLoadDllEDR 在进程早期(即 加载 时kernel32.dll,即 之前)取得控制权g_pfnSE_DllLoaded。这使得 EDR 能够先于我们获得对进程的控制权。但是,正如我们在调用图中看到的那样,g_pfnSE_DllLoaded在 EDR 初始化之前,我们获得了控制权,此时我们取得控制权。这意味着,尽管 EDR 先取得控制权,但我们仍然可以破坏其检测措施的初始化,因为我们可以在 DLL 初始化之前取得控制权,从而阻止 EDR 加载它们。
我们还观察到,大多数 EDR 最初通过 shellcode 加载kernel32.dll和kernelbase.dll,遵循正常执行流程。然后,它们通过加载挂钩 DLL LdrLoadDll。请记住,g_pfnSE_DllLoaded是在初始化部分执行的LdrLoadDLL,在本例中是kernelbase.dll。这远在 shellcode 加载 EDR 的检测措施之前。理论上,在这个阶段,我们可以移除对的挂钩LdrLoadDll,恢复到加载的原始代码路径kernel32.dll,然后继续执行,绕过 EDR 的加载过程。
在本博客中讨论的回调指针的使用方法可能有很多,可以阻止用户模式 EDR 检测措施。我们介绍了一种可能的方法,可以通过利用g_pfnSE_DllLoaded回调指针将其集成到早期级联注入中。这将允许通过早期级联注入注入的植入程序更隐秘地运行,从而进一步逃避 EDR 检测。
结论
在这篇博文中,我们探讨了如何在 Windows 中创建进程,重点关注进程创建的用户模式部分。我们提供了一个调用图,概述了进程创建期间的关键事件。然后,我们研究了 Early Bird APC 注入的工作原理以及与用户模式部分的交互方式,特别是在执行排队的 APC 时。之后,我们讨论了 EDR-Preloading,它向我们展示了如何通过覆盖指针在进程创建期间实现代码执行。这促使我们进一步调查并发现了一个新的指针。但是,无法通过它执行功能齐全的代码。通过将 Early Bird APC 的 APC 排队元素与受 EDR-Preloading 启发的新指针相结合,我们开发并解释了 Early Cascade Injection。最后,我们重点介绍了这种技术的主要功能。我希望您发现调用图和我们一样有用 - 概述了进程创建,揭示了 EDR 安全措施的时间,并展示了 Early Cascade Injection 如何与进程创建交互。
原文始发于微信公众号(Ots安全):早期级联注入简介:从 Windows 进程创建到隐秘注入
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论