Windows 进程、恶意异常和您:内存区域

admin 2024年6月19日19:19:58评论7 views字数 10327阅读34分25秒阅读模式

Windows 进程、恶意异常和您:内存区域

在红队行动中,主机上出现端点检测和响应 (EDR) 的可能性比几年前越来越高。当在主机上启动植入程序时,无论是在磁盘上还是加载到内存中,都有很多需要考虑的地方。在这篇文章中,我们将重点介绍 EDR 的一个非常具体的组件:内存扫描仪。

内存扫描器的作用非常直观。它扫描进程的内存并尝试识别内存区域内的非标准属性,以确定进程是否需要额外的分析和/或遏制。

社区在实施内存扫描器来识别恶意活动方面做得非常出色,并且红队成员也采用它们作为对自己的植入物进行 QA 的一种手段:

  • pe-sieve by Hasherezade

  • Moneta by Forrest Orr

  • Hunt-Sleeping-Beacons by thefLinkk

为了获得额外的分数,组织可以将这些纳入他们自己的检测策略中——然而,这些类型的工具会在流程中寻找非常具体的异常,因此可能会产生误报。

对于 EDR 供应商而言,这些扫描器的较小组件可能已包含在其工具包中,但必须付出大量努力才能确保误报不会进入生产环境,更不用说客户环境了。然而,它们在 EDR 中的用途略有不同 - 通常,当内存扫描器指示器之一被击中时,它将触发对该进程的进一步分析。这可能是已知恶意软件签名、该特定进程的日志分析等。EDR 极不可能因为 RWX 已在进程中分配而对端点创建警报。在我们进入本系列时,我们将展示内存扫描器在扫描所有内容时可以创建的大量误报。但是,这可能会导致 EDR 进一步调查该进程(作为一个简单的示例)。

在这篇博客中,我们将研究内存扫描器正在查看什么以及为什么查看,然后我们将从命令和控制 (C2) 植入物中识别出一些唾手可得的成果。

1. 流程结构

最简单的形式是,进程是一个正在执行的程序。从本质上讲,Windows 是一个面向对象的系统。这意味着 Windows 的每个组件本质上都会归结为某种对象。对于进程,Windows 内核将其称为 EPROCESS 结构。但是,如果往上一级,该结构就会简化为进程环境块(PEB)。

typedef struct _PEB {   BYTE Reserved1 [2] ;   BYTE BeingDebugged;   BYTE Reserved2 [1] ;   PVOID Reserved3 [2] ;   PPEB_LDR_DATA Ldr;   PRTL_USER_PROCESS_PARAMETERS ProcessParameters;   PVOID Reserved4 [3] ;   PVOID AtlThunkSListPtr;   PVOID Reserved5;   ULONG Reserved6;   PVOID Reserved7;   ULONG Reserved8;   ULONG AtlThunkSListPtr32;   PVOID Reserved9 [45] ;   BYTE Reserved10 [96] ;   PPS_POST_PROCESS_INIT_ROUTINE PostProcessInitRoutine;   BYTE Reserved11 [128] ;   PVOID Reserved12 [1] ;   ULONG SessionId; } PEB, *PPEB;

从这个结构中,我们可以获取进程名称、当前目录、已加载的动态链接库 (DLL) 等信息。这是我们将要大量使用的结构。

为了简化这一过程,我们将重点关注流程的三个组成部分:

  1. 内存区域

  2. 线程

  3. 已加载的 DLL

要了解有关 PEB 的更多信息,建议阅读“进程环境块 (PEB) 的剖析(Windows 内部) ”。

2. 背景介绍

稍微介绍一下背景信息,本次演示使用的样本将是Maelstrom系列未发布/概念验证的 C2 ,因为它包含嵌入式妥协指标,这对于本次演示来说是完美的。

Windows 进程、恶意异常和您:内存区域

图 1 Maelstrom 植入物现有

它会联系本地 IP 地址,请求反射 DLL,然后在内存中执行它。我们将寻找最后一步。

最后要说明的是:本文中用于评估该过程的框架将不会发布,但我们将尽力提供实现本博客每个组件的源代码。所讨论的工具称为 Fennec,我们将在整篇文章中介绍它。

3. 枚举内存区域

以 explorer.exe 为例,让我们使用Process Hacker查看内存区域。具体操作如下:找到一个进程,双击,然后转到“内存”选项卡。

Windows 进程、恶意异常和您:内存区域

图 2 Process Explorer 内存区域

以编程方式实现此目的的方法是VirtualQueryEx调用。

SIZE_T VirtualQueryEx(  [in]           HANDLE                    hProcess,  [in, optional] LPCVOID                   lpAddress,  [out]          PMEMORY_BASIC_INFORMATION lpBuffer,  [in]           SIZE_T                    dwLength);

这需要几个参数:

  • 进程句柄

  • 要查询的基址

  • 指向结构的指针

  • 前一个参数的大小

这个调用中最重要的部分是我们期望从中获得的内容:MEMORY_BASIC_INFORMATION。

typedef  struct  _MEMORY_BASIC_INFORMATION {   PVOID BaseAddress;   PVOID AllocationBase;   DWORD AllocationProtect;   WORD PartitionId;   SIZE_T RegionSize;   DWORD State;   DWORD Protect;   DWORD Type; } MEMORY_BASIC_INFORMATION, *PMEMORY_BASIC_INFORMATION;

这个结构基本上会创建 Process Hacker 屏幕截图中的大部分区域,这将为我们提供分析进程内存所需的 99% 的信息!

由于我们将要生产的扫描仪有一个额外的成员,并且为了允许以后的扩展,因此定义了一个新的结构。

typedef struct REGION_ { LPVOID BaseAddress = nullptr;       LPVOID AllocationBase = nullptr;       WORD PartitionId = 0 ;       DWORD Size = 0 ;       DWORD ActiveProtect = 0 ;       DWORD InitialProtect = 0 ;       DWORD State = 0 ;       DWORD Type = 0 ;       std:: string Use = "" ; } Region;

在这种情况下,使用将保存与该区域关联的 DLL (如果存在)。

在我们处理所有区域之前还有最后一件事 — — 让我们快速记录一下我们实际需要的每个结构成员:

  • 基地址:内存区域的基地址

  • 分配基址:由VirtualAlloc创建的页面范围的基址

  • 区域大小:从所有页面的基地址开始的区域的大小

  • 状态:已提交、已释放还是已保留

  • 主动保护:自访问之日起对该区域的访问进行保护

  • 初始保护:最初分配的保护

  • 类型:是私有内存、图像内存还是映射内存(稍后会详细介绍)

  • 用途:该区域存在的原因

定义的每个结构都将放入一个向量(对象数组)中。这是查询每个区域的函数,按当前区域的大小递增。然后,我们构建区域结构并将其添加到向量中。

std::vector<FENNEC::Processes::Region> FENNEC::Processes::GetAllRegions(HANDLE hProcess){    std::vector<FENNEC::Processes::Region> Regions;    MEMORY_BASIC_INFORMATION mbi = { 0 };    LPVOID offset = 0;    while (VirtualQueryEx(hProcess, offset, &mbi, sizeof(mbi)))    {        if (mbi.RegionSize > 0)        {            offset = (LPVOID)((DWORD_PTR)mbi.BaseAddress + mbi.RegionSize);            FENNEC::Processes::Region Region;            Region.BaseAddress = mbi.BaseAddress;            Region.AllocationBase = mbi.AllocationBase;            Region.PartitionId = mbi.PartitionId;            Region.Size = mbi.RegionSize;            Region.ActiveProtect = mbi.Protect;            Region.InitialProtect = mbi.AllocationProtect;            Region.State = mbi.State;            Region.Type = mbi.Type;            Region.Use = FENNEC::Processes::GetRegionUse(hProcess, Region);            Regions.push_back(Region);        }    }    return Regions;}

至于GetRegionUse,这只是以下函数的包装器。

std::string FENNEC::Processes::GetModulePath(HANDLE hProcess, HMODULE hModule){    CHAR Path[MAX_PATH];    if (K32GetModuleFileNameExA(hProcess, hModule, Path, sizeof(Path) / sizeof(CHAR)))    {        return std::string(Path);    }    else    {        return "";    }}

GetModuleFileNameExA接收进程句柄和模块(基址),然后尝试检索其名称。如果成功,则该区域具有“用途”。这意味着内存区域归属于某个对象。我们可以证明这一点。

在下面的屏幕截图中,我们可以看到WINWORD.EXE 的内存区域,有两点需要注意。

Windows 进程、恶意异常和您:内存区域

图 3 WINWORD.EXE 内存使用情况

首先,Use 列中填入了各种 DLL。其次,内存类型是Image: Commit (MEM_IMAGE).

当 DLL 被加载到进程中时,它的内存区域将是一个映射映像(MEM_IMAGE),并且基地址将是 DLL 的地址 - 这就是 的来源Use,这也是我们上面复制的内容。

举例来说,其中一个结构如下所示。

Windows 进程、恶意异常和您:内存区域

图 4 区域结构示例

这样,枚举部分就完成了。接下来,解析它是否不好。

4. 识别内存区域中的恶意属性

有很多方法可以过滤内存区域以将其标记为恶意。在这篇博文中,我们将介绍两个容易实现的方法——私有内存区域中的 RWX 和 MZ 标头——以确保这篇博文不会太长。

4.1. PAGE_EXECUTE_READWRITE

在本博客中提到的所有技术中,这是最容易识别的。与其他技术一样,如果内存扫描器检测到其中任何一种技术,并不一定意味着它们是恶意的——这只是进一步枚举的潜在指标。

以下是检查 RWX 的代码。

void Scanner::HuntRWX(std::vector<FENNEC::Processes::Region> Regions, FENNEC::Comms::Common Common){  for (FENNEC::Processes::Region& Region : Regions)  {    if (Region.ActiveProtect == PAGE_EXECUTE_READWRITE)    {      nlohmann::json Json;      Json["method"] = "RWX";      Json["base_address"] = FENNEC::Strings::LPVOID2StringA(Region.BaseAddress);      Json["use"] = Region.Use;      Json["allocation_base"] = FENNEC::Strings::LPVOID2StringA(Region.AllocationBase);      Json["partition_id"] = std::to_string(Region.PartitionId);      Json["region_size"] = std::to_string(Region.Size);      Json["region_protection_active"] = FENNEC::Strings::ProtectToString(Region.ActiveProtect);      Json["region_allocation_initial"] = FENNEC::Strings::ProtectToString(Region.InitialProtect);      Json["region_state"] = FENNEC::Strings::AllocateToString(Region.State);      Json["region_type"] = FENNEC::Strings::TypeToString(Region.Type);      std::string Log = FENNEC::Comms::ConvertCommonLogStructureToJson(Common, Json);      FENNEC::Logger::Good("RWX Identified: %sn", Log.c_str());      FENNEC::Logger::WriteLogToFile(LOG_TYPE, Log);    }  }}

本质上,我们检查ActiveProtect 结构的成员是否PAGE_EXECUTE_READWRITE简单。

然后我们运行扫描仪。

Windows 进程、恶意异常和您:内存区域

图 5 RWX 扫描日志

美化JSON,这是完整的日志。

{  "data": {    "allocation_base": "0x00000000001E0000",    "base_address": "0x00000000001E0000",    "method": "RWX",    "partition_id": "0",    "region_allocation_initial": "PAGE_EXECUTE_READWRITE",    "region_protection_active": "PAGE_EXECUTE_READWRITE",    "region_size": "73728",    "region_state": "MEM_COMMIT",    "region_type": "MEM_PRIVATE",    "use": ""  },  "event_category": "Memory Scanner",  "event_time": "Tue Sep 6 10:07:34 2022",  "guid": "e861eb49-08ab-427f-95d7-e8116475c1e8",  "image_name": "maelstrom.unsafe.x64.exe",  "image_path": "\Device\HarddiskVolume11\maelstrom\agent\stage0\bin\maelstrom.unsafe.x64.exe",  "parent_procecess": 12652,  "process_id": 15372}

这种日志结构在扫描仪内的所有技术中都很常见,它提供了大量有助于进一步分析的背景信息。例如,在本例中,它被分配为 RWX,并且根本没有改变。但是,如果将其分配为 RW 并切换到 RX,那么这也可能是恶意软件,因为这是 99% 的植入程序所采用的过程。

对于 EDR,在内存扫描期间检查从 RW 到 RX 的保护更改并不经常执行。调整扫描仪以扫描所有进程,它会在主机上生成 498 个条目。

Windows 进程、恶意异常和您:内存区域

图 6 找到 498 个 RWX 条目

通过在 ELK 内部检查这些信息,我们更进一步发现有相当多的进程使用了 RWX。

Windows 进程、恶意异常和您:内存区域

图 7 图像到 RWX 日志条目

需要澄清的是,左列是进程名称,右列是该进程分配了 RWX 的日志条目数量。

4.2. 私有内存区域中的 MZ 标头

这种方法不太常见,理解起来也有点复杂。当加载 DLL 时,它会被标记为MEMORY_BASIC_INFORMATIONMEM_IMAGE中的一种类型。

Windows 进程、恶意异常和您:内存区域

图 8 MEMORY_BASIC_INFORMATION 类型

我们可以通过打开 Process Hacker、找到一个进程并导航到该Memory部分来看到这一点。

Windows 进程、恶意异常和您:内存区域

图 9 MEM_IMAGE 内存区域

在这个列表中,还会有标记为已映射的区域,这相当于类型MEM_MAPPED。

Windows 进程、恶意异常和您:内存区域

图 10 MEM_MAPPED 内存区域

查看所有MEM_PRIVATE分配,它们往往是进程/DLL 内用于满足其需要的区域。

Windows 进程、恶意异常和您:内存区域

图 11 MEM_PRIVATE 内存区域

举个例子,让我们分配一大块内存。

LPVOID pAddress = VirtualAlloc ( nullptr,409600, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE );   memset ( pAddress ,'a',409600 );

打开内存区域,我们可以看到我们的分配是Private Memory。

Windows 进程、恶意异常和您:内存区域

图 12 VirtualAlloc 创建私有内存

然后再次使用malloc(和 VirtualQuery 以便我们可以找到它在哪里)。

  LPVOID pAddress = (LPVOID) malloc ( 409600 );   memset (pAddress, 'a' , 409600 );   MEMORY_BASIC_INFORMATION mbi = { 0 };   VirtualQuery (pAddress, &mbi, sizeof mbi);

结果如下。

Windows 进程、恶意异常和您:内存区域

图 13 Malloc 创建私有内存

正如我们在两种情况下看到的,内存被分配为私有的,这意味着当在进程内分配内存来使用缓冲区执行某些操作时,就会创建这样的区域。

因此,如果在此区域内发现 DLL,则非常可疑。如果真正的进程需要合法的 DLL,它会适当地加载它 — 它会在运行时将其作为依赖项加载,或者使用类似LoadLibraryA的程序动态加载它。

现在我们明白了为什么在私有内存中看到 DLL 有点奇怪,让我们看看如何识别它。

我们首先像之前一样识别所有内存区域,然后将其结构放入向量中。现在是时候进行解析了。

我们能做的第一件事就是忽略MEM_IMAGE和MEM_MAPPED。由于这更像是一个概念验证,我们不需要关心它们。话虽如此,EDR 对这些类型的反应会有所不同,这种逻辑极不可能发生。但我们现在会这样做。

if (Region.Type == MEM_MAPPED || Region.Type == MEM_IMAGE){    continue;}

接下来我们定义一些将要用到的东西。

BOOL bMzFound = FALSE;BOOL bIsDLLBacked = FALSE;std::vector<unsigned char> bytes = { 0x4d, 0x5a };PCHAR lpBuffer = static_cast<PCHAR>(malloc(Region.Size));

注意字节向量。0x4d 和 0x5a 是 MZ 的十六进制值。然后我们分配空间,以便读取整个区域。

然后我们使用ReadProcessMemory读取该区域。

BOOL bRead = ReadProcessMemory(hProcess, (LPVOID)Region.BaseAddress, lpBuffer, Region.Size, NULL);if (bRead == FALSE){      free(lpBuffer);      continue;}

在这个例子中,我们获取该区域的前两个字节并将它们放入向量中。

std::vector<unsigned char> vectorBuffer(lpBuffer, lpBuffer + 2);

只需在恶意区域的开头添加三个 0(000),就可以避免这种逻辑。

这样,我们将新创建的 2 字节向量与该MZ向量进行比较。

BOOL FENNEC::Strings::CompareVectors(std::vector<unsigned char> a, std::vector<unsigned char> b){  if (std::equal(a.begin(), a.end(), b.begin()))  {    return TRUE;  }  else  {    return FALSE;  }}

到现在为止,它可能已经完成了。但为了确保万无一失,还需要先进行一些检查。

如果返回“true”响应,则扫描仪将验证该区域是否属于进程中的任何 DLL。这不是严格要求的,但值得。要实现这一点,有两种方法。

首先,我们获取进程中的每个 DLL 并比较基地址。

  std::vector<FENNEC::Processes::Module> modules = FENNEC::Processes::GetModules(hProcess);  for (FENNEC::Processes::Module& Module : modules)  {    if (Region.AllocationBase == Module.BaseAddress)    {      bIsDLLBacked = TRUE;      break;    }  }

或者我们可以通过解析PPEB_LDR_DATA结构手动完成此操作。

BOOL FENNEC::PEBLOCK::IsBaseAddressWithDll(LPVOID BaseAddress){  PPEB_LDR_DATA Ldr = FENNEC::PEBLOCK::Peb->Ldr;  LIST_ENTRY* ModuleList = NULL;  BOOL bDllIsBacked = FALSE;  ModuleList = &Ldr->InMemoryOrderModuleList;  LIST_ENTRY* pStartListEntry = ModuleList->Flink;  for (LIST_ENTRY* pListEntry = pStartListEntry; pListEntry != ModuleList; pListEntry = pListEntry->Flink)  {    LDR_DATA_TABLE_ENTRY* pEntry = (LDR_DATA_TABLE_ENTRY*)((BYTE*)pListEntry - sizeof(LIST_ENTRY));    std::wstring wsName(pEntry->BaseDllName.Buffer);    std::wstring wsPath(pEntry->FullDllName.Buffer);    std::string modName = FENNEC::Strings::StringW2StringA(wsName);    std::string modPath = FENNEC::Strings::StringW2StringA(wsPath);    if (BaseAddress == pEntry->DllBase)    {      bDllIsBacked = TRUE;      break;    }  }  return bDllIsBacked;}

无论哪种方式,这都会返回“错误”响应,因为内存区域不是 DLL 的基地址。

针对植入过程,它确定一个区域。

Windows 进程、恶意异常和您:内存区域

图 14 在 MEM_PRIVATE 中找到 MZ 标头

这是完整的日志。

{  "data": {    "allocation_base": "0x00000000001E0000",    "base_address": "0x00000000001E0000",    "method": "Memory Allocation without DLL Backing",    "partition_id": "0",    "region_allocation_initial": "PAGE_EXECUTE_READWRITE",    "region_protection_active": "PAGE_EXECUTE_READWRITE",    "region_size": "73728",    "region_state": "MEM_COMMIT",    "region_type": "MEM_PRIVATE",    "use": ""  },  "event_category": "Memory Scanner",  "event_time": "Tue Sep  6 11:57:57 2022",  "guid": "59cd7a5a-53aa-4ca8-91b4-d76e8feecab1",  "image_name": "maelstrom.unsafe.x64.exe",  "image_path": "\Device\HarddiskVolume11\maelstrom\agent\stage0\bin\maelstrom.unsafe.x64.exe",",  "parent_procecess": 12652,  "process_id": 15372}

与 RWX 一样,此检测策略报告区域。因此,上面的结构完全相同。但是,这次methodJSON 中的 发生了变化。

"method": "Memory Allocation without DLL Backing"

如果我们在主机上的每个进程上运行此扫描器,则只有一个区域会被命中。

Windows 进程、恶意异常和您:内存区域

图 15 在 MEM_PRIVATE 中发现单个 MZ 标头

我们可以通过打开 Process Hacker 并找到区域 ( 0x00000000001E0000) 来验证这一点。

Windows 进程、恶意异常和您:内存区域

图 16 带有 MZ 标头的 MEM_PRIVATE

与 RWX 检查相比,这个指标更加准确,而且更有说服力。

5. 结论

就这一点而言,pe-sieve等工具在更详细地检测恶意活动方面表现更好,如果需要植入开发,则推荐使用此类工具。至于 EDR,某些实用程序可能过于耗费性能,不太可能使用 - 但是,这些属性将被使用,并且通常用作触发进一步查询、规则或脚本的基础。

Windows Processes, Nefarious Anomalies, and You: Memory Regionshttps://www.trustedsec.com/blog/windows-processes-nefarious-anomalies-and-you-memory-regions

原文始发于微信公众号(Ots安全):Windows 进程、恶意异常和您:内存区域

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2024年6月19日19:19:58
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   Windows 进程、恶意异常和您:内存区域https://cn-sec.com/archives/2864962.html

发表评论

匿名网友 填写信息