关于PE文件的内存加载分享
字数 1543 2025-08-05 08:19:19
PE文件内存加载技术详解
一、内存加载概述
内存加载是一种将PE格式文件(DLL或EXE)直接从内存加载执行的技术,无需通过传统API如LoadLibrary操作。这种技术在攻防对抗中具有重要意义:
-
优势:
- 文件不存在于磁盘,规避静态检测
- 避免文件加载触发的内核回调
- 加载的程序不会在PEB和进程中呈现
- 规避敏感调用链检测(如cmd→灰进程)
-
主要方法:
- 针对PE文件:编写PELoader模拟
LoadLibrary操作 - 针对.NET程序集:利用C#反射特性(
Assembly.Load)
- 针对PE文件:编写PELoader模拟
二、PELoader实现原理
1. 核心流程
-
获取必要API地址:
- 通过PEB获取
LoadLibrary、GetProcAddress、VirtualAlloc等关键API地址
- 通过PEB获取
-
内存映射PE文件:
- 根据
SizeOfImage(PE可选头字段)申请内存空间 - 将DLL头部和各节区复制到分配的内存
- 设置对应内存权限(需注意对齐问题,
SectionAlignment与FileAlignment可能不同)
- 根据
-
修复重定位表:
- 计算实际加载地址与偏好基址(
ImageBase)的偏移量 - 根据重定位表将所有需要修复的地址加上该偏移量
- 计算实际加载地址与偏好基址(
-
修复IAT表:
- 遍历所有导入的DLL
- 对每个导入函数,根据序号或名称补丁函数地址
-
跳转执行:
- 跳转到PE文件的OEP(Original Entry Point)执行
2. 实现细节
以ReflectiveDLLInjection为例:
// 伪代码示例
void ReflectiveLoader() {
// 1. 计算DLL基址
PVOID pDllBase = GetCurrentImageBase();
// 2. 获取必要API
PLOADLIBRARYA pLoadLibraryA = GetProcAddressFromPEB("LoadLibraryA");
PGETPROCADDRESS pGetProcAddress = GetProcAddressFromPEB("GetProcAddress");
// 3. 映射PE到内存
PVOID pMappedDll = VirtualAlloc(NULL, pNtHeaders->OptionalHeader.SizeOfImage,
MEM_RESERVE|MEM_COMMIT, PAGE_EXECUTE_READWRITE);
// 4. 复制节区
CopyHeadersAndSections(pDllBase, pMappedDll);
// 5. 修复重定位
ProcessRelocations(pMappedDll);
// 6. 修复IAT
ResolveImports(pMappedDll, pLoadLibraryA, pGetProcAddress);
// 7. 调用DllMain
CallDllMain(pMappedDll, DLL_PROCESS_ATTACH);
}
三、内存加载的检测规避技术
1. RWX内存问题
传统实现会分配RWX(可读可写可执行)内存,这非常可疑。改进方案:
- 分配RX(可读可执行)和RW(可读可写)分离的内存区域
- 仅在需要修改时设为RW,执行时设为RX
2. Module Stomping技术
原理:
- 将合法Windows DLL注入目标进程
- 用shellcode覆盖DLL的入口点
- 创建新线程执行shellcode
优势:
- 可执行内存对应合法DLL文件,规避映像文件检测
- 内存区域属性看起来合法
实现示例:
// 1. 注入合法DLL(如amsi.dll)
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, targetPid);
PVOID pRemoteBuf = VirtualAllocEx(hProcess, NULL, sizeof(dllPath), MEM_COMMIT, PAGE_READWRITE);
WriteProcessMemory(hProcess, pRemoteBuf, (LPVOID)dllPath, sizeof(dllPath), NULL);
CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)LoadLibraryW, pRemoteBuf, 0, NULL);
// 2. 查找注入DLL的基址
HMODULE hModules[256];
DWORD cbNeeded;
EnumProcessModules(hProcess, hModules, sizeof(hModules), &cbNeeded);
for (HMODULE hMod : hModules) {
if (IsTargetModule(hProcess, hMod)) {
targetModule = hMod;
break;
}
}
// 3. 获取入口点并覆盖
PIMAGE_DOS_HEADER pDosHeader = ReadProcessMemory(hProcess, targetModule, sizeof(IMAGE_DOS_HEADER));
PIMAGE_NT_HEADERS pNtHeaders = ReadProcessMemory(hProcess, targetModule + pDosHeader->e_lfanew, sizeof(IMAGE_NT_HEADERS));
PVOID pEntryPoint = targetModule + pNtHeaders->OptionalHeader.AddressOfEntryPoint;
WriteProcessMemory(hProcess, pEntryPoint, shellcode, sizeof(shellcode), NULL);
// 4. 执行
CreateRemoteThread(hProcess, NULL, 0, (LPTHREAD_START_ROUTINE)pEntryPoint, NULL, 0, NULL);
四、Loader类型比较
1. 内置式Loader(Cobalt Strike风格)
特点:
- 修改DOS头使其包含可直接调用
ReflectiveLoader的shellcode ReflectiveLoader函数内置在DLL中- 通过硬编码偏移计算
ReflectiveLoader位置
示例:
4D 5A ; "MZ" DOS头
41 52 55 48 89 E5 48 ; 开始PE头
81 EC 20 00 00 00 ; sub rsp, 0x20
48 8D 1D EA FF FF FF ; lea rbx, [rip - 0x16]
48 89 DF ; mov rdi, rbx
48 81 C3 44 64 01 00 ; add rbx, 0x16444
FF D3 ; call rbx
2. 前置式Loader
特点:
ReflectiveLoader放在DLL前面而非内部- 可反射加载任意PE文件,无需源码
- 不需要导出函数,规避相关检测
优势:
- 更通用,不限于特定DLL
- 降低特征检测风险
五、实践注意事项
-
内存权限管理:
- 避免长时间保持RWX权限
- 使用
VirtualProtect动态调整权限
-
异常处理:
- 确保加载失败时能安全退出
- 处理内存分配失败等情况
-
兼容性考虑:
- 处理32/64位差异
- 考虑不同Windows版本特性
-
规避技巧:
- 使用合法模块镂空(如
amsi.dll) - 延迟执行技术
- 间接系统调用
- 使用合法模块镂空(如
六、参考实现
-
开源项目:
- ReflectiveDLLInjection
- Donut (支持Module Overloading)
-
关键数据结构:
typedef struct _IMAGE_RELOCATION_ENTRY {
WORD Offset : 12;
WORD Type : 4;
} IMAGE_RELOCATION_ENTRY;
typedef struct _IMAGE_IMPORT_DESCRIPTOR {
union {
DWORD Characteristics;
DWORD OriginalFirstThunk;
};
DWORD TimeDateStamp;
DWORD ForwarderChain;
DWORD Name;
DWORD FirstThunk;
} IMAGE_IMPORT_DESCRIPTOR;
七、防御检测建议
-
检测点:
- 非常规内存权限(RWX)
- 未关联映像文件的执行内存
- 模块镂空行为
- PEB中缺失的模块
-
防御措施:
- 监控关键API调用链
- 检测内存中的PE签名
- 行为分析而不仅是静态检测
通过深入理解PE内存加载技术原理,安全人员可以更好地开发防御措施,而渗透测试人员则能更有效地规避检测。