模块伪装与调用栈欺骗
原创
pandazhengzheng
pandazhengzheng
安全分析与研究
2026年8月29日 22:00
广东
在小说阅读器读本章
去阅读
在公众号小说中沉浸阅读
目录
- 问题背景:内存扫描与调用栈回溯
- Windows 内存管理基础
- Module Stomping 原理
- Module Stomping 实现详解
- Module Stomping 的变体与进阶
- Module Stomping 的检测与规避
- 调用栈回溯的内部机制
- Call Stack Spoofing 原理
- Call Stack Spoofing 实现详解
- Call Stack Spoofing 的检测与规避
- 两种技术的组合与完整链路
- 小结
1. 问题背景:内存扫描与调用栈回溯
1.1 shellcode 落地的特征
无论用 APC、Fiber 还是其他执行载体,shellcode 都需要驻留在一段可执行内存中。这段内存的特征是 EDR 内存扫描的重点:
| 内存属性组合 | 含义 | EDR 可疑度 |
|---|---|---|
MEM_PRIVATE + PAGE_EXECUTE_READWRITE |
纯 shellcode,可写可执行 | 极高 |
MEM_PRIVATE + PAGE_EXECUTE_READ |
纯 shellcode,只读执行 | 高 |
MEM_IMAGE + PAGE_EXECUTE_READ + 内容与磁盘一致 |
合法已加载模块 | 低 |
MEM_IMAGE + PAGE_EXECUTE_READ + 内容与磁盘不一致 |
module stomping/hollowing | 中-高 |
MEM_MAPPED + PAGE_EXECUTE_READ |
映射的可执行段(如反射加载的 PE) | 中 |
EDR 的初筛规则:MEM_PRIVATE + PAGE_EXECUTE* 几乎必然被标记并扫描。因此,免杀的首要目标是让 shellcode 不驻留在 Private+Execute 内存。
1.2 调用栈的特征
当 shellcode 调用 ntdll 中的系统服务(即便用 Indirect Syscalls,最近一层返回地址合法,更深层仍可能暴露)时,EDR 回溯调用栈会发现:
- 返回地址落在
MEM_PRIVATE(shellcode 直接调用); - 返回地址落在未在 PEB
Ldr链表登记的模块; - 调用链不自然(如
NtAllocateVirtualMemory的调用方不是kernel32/ntdll,而是某私有内存)。
Module Stomping 解决\"内存属性\"问题,Call Stack Spoofing 解决\"调用栈\"问题。两者常组合使用。
2. Windows 内存管理基础
2.1 内存区域的三种类型
- MEM\_PRIVATE:由
VirtualAlloc/VirtualAllocEx提交的私有内存,不与任何文件关联; - MEM\_MAPPED:由
MapViewOfFile/NtMapViewOfSection映射的 section,可对应文件或 pagefile; - MEM\_IMAGE:由映像 loader 映射的 PE 文件(
LoadLibrary、NtMapViewOfSection以SEC_IMAGE创建的 section),有对应的磁盘文件(或内存中的 PE)。
关键:MEM_IMAGE 是 loader 的产物,操作系统与 EDR 将其视为\"已加载模块\",享有最低可疑度。
2.2 VAD(Virtual Address Descriptor)
内核通过 VAD 树管理进程的虚拟地址空间。每个 VAD 节点记录一段虚拟地址范围的属性(类型、保护、对应 section/control area)。NtQueryVirtualMemory 返回的 MemoryInformation 即来自 VAD。
关键:VAD 中的 Type 字段(Private/Mapped/Image)由映射方式决定,而非内容。一旦通过 SEC_IMAGE section 映射,VAD 即标记为 MEM_IMAGE,即便后续覆写内容,VAD 类型不变。
2.3 PEB Ldr 链表
进程的 PEB->Ldr 包含三条双向链表:
InLoadOrderModuleList:按加载顺序;InMemoryOrderModuleList:按内存地址;InInitializationOrderModuleList:按初始化顺序。
每个节点是一个 LDR_DATA_TABLE_ENTRY,记录模块基址、大小、名称、路径等。EDR 通过遍历这些链表判断\"某地址是否属于已加载模块\"。
关键:MEM_IMAGE 内存不一定在 Ldr 链表中——通过 NtMapViewOfSection 以 SEC_IMAGE 映射但不调用 LdrLoadDll,VAD 是 MEM_IMAGE 但 Ldr 中无登记。EDR 会检查此不一致。
3. Module Stomping 原理
3.1 核心思路
Module Stomping 的目标是:让 shellcode 驻留在一个被操作系统视为合法映像(MEM_IMAGE)的内存区域中,从而通过 EDR 的\"是否为私有可执行内存\"与\"是否为已加载模块\"两项初筛。
具体做法:
- 获取一个合法的
MEM_IMAGE区域:加载一个 DLL(或映射一个 PE section); - 在该区域中找到一段可写(或可改为可写)的可执行空间:通常是
.text段; - 将 shellcode 覆写进去;
- 跳转到该区域执行 shellcode。
此时 shellcode 所在内存的 VAD 类型为 MEM_IMAGE,保护属性为 PAGE_EXECUTE_READ,从 VAD 与保护属性看与合法代码无异。
3.2 为什么有效
- VAD 类型检查通过:
MEM_IMAGE而非MEM_PRIVATE; - 保护属性检查通过:
PAGE_EXECUTE_READ而非PAGE_EXECUTE_READWRITE; - RIP 落点检查通过:执行时 RIP 落在
MEM_IMAGE区域; - 初级内存扫描通过:EDR 的\"扫描所有 Private+Execute 内存\"不会扫描此区域(它是 Image+Execute)。
3.3 剩余检测面
- 映像完整性校验:EDR 将内存中的映像与磁盘文件比对,若
.text被覆写则不一致; Ldr链表检查:若映射方式未登记到Ldr,EDR 发现\"VAD 是 Image 但 Ldr 无记录\";- 被 stomp 的 DLL 功能破坏:若该 DLL 被其他代码调用,覆写会破坏功能。
4. Module Stomping 实现详解
4.1 选择目标 DLL
选择标准:
- 磁盘存在且签名合法:能通过
LoadLibrary正常加载; - 低关注度:不是 EDR 重点监控的 DLL(如
ntdll、kernel32); - 不会被频繁调用:覆写其
.text不会破坏进程功能; .text足够大:能容纳 shellcode。
典型选择:
C:\\Windows\\System32\\下的冷门 DLL:如xpsservices.dll、wmvcore.dll、msftedit.dll;- 一个攻击者自带但签名合法的 DLL(若有签名能力);
- 一个已加载但未被使用的 DLL。
4.2 加载目标 DLL
方式 A:LoadLibraryEx
HMODULE hMod = LoadLibraryEx(\"xpsservices.dll\", NULL, DONT_RESOLVE_DLL_REFERENCES);
DONT_RESOLVE_DLL_REFERENCES 加载 DLL 但不调用 DllMain、不解析导入表,减少副作用。DLL 的 .text 被映射为 PAGE_EXECUTE_READ。
方式 B:NtMapViewOfSection(手动映射)
HANDLE hFile = CreateFile(..., \"xpsservices.dll\", ...);
HANDLE hSection;
NtCreateSection(&hSection, SECTION_ALL_ACCESS, NULL, NULL, PAGE_READONLY, SEC_IMAGE, hFile);
PVOID base = NULL;
SIZE_T viewSize = 0;
NtMapViewOfSection(hSection, NtCurrentProcess(), &base, 0, 0, NULL, &viewSize, ViewShare, 0, PAGE_READONLY);
手动映射不经过 loader,不登记到 Ldr 链表。VAD 是 MEM_IMAGE 但 Ldr 无记录。
权衡:
LoadLibraryEx登记Ldr,但触发映像加载回调(EDR 感知\"加载了冷门 DLL\");NtMapViewOfSection不触发映像加载回调,但Ldr无记录,EDR 可能发现不一致。
4.3 定位 .text 段
解析 PE 头:
PIMAGE_DOS_HEADER dos = (PIMAGE_DOS_HEADER)base;
PIMAGE_NT_HEADERS nt = (PIMAGE_NT_HEADERS)((BYTE*)base + dos->e_lfanew);
PIMAGE_SECTION_HEADER sec = IMAGE_FIRST_SECTION(nt);
for (int i = 0; i < nt->FileHeader.NumberOfSections; i++) {
if (memcmp(sec[i].Name, \".text\", 5) == 0) {
textVa = (BYTE*)base + sec[i].VirtualAddress;
textSize = sec[i].Misc.VirtualSize;
break;
}
}
4.4 覆写 shellcode
DWORD oldProt;
VirtualProtect(textVa, shellcodeSize, PAGE_READWRITE, &oldProt);
memcpy(textVa, shellcode, shellcodeSize);
VirtualProtect(textVa, shellcodeSize, PAGE_EXECUTE_READ, &oldProt);
关键:
- 先
PAGE_READWRITE写入,再改回PAGE_EXECUTE_READ,\\避免PAGE_EXECUTE_READWRITE\\(RWX 是强特征); - 但
VirtualProtect改为可写、再改为可执行,两次都触发 ETW-TIEtwTiLogProtectExecVm; - 改回
PAGE_EXECUTE_READ后,shellcode 所在内存属性为MEM_IMAGE + PAGE_EXECUTE_READ,与合法代码无异。
4.5 跳转执行
// 通过函数指针跳转
((void(*)())textVa)();
或结合 Fiber / APC,将执行入口设为 textVa。
5. Module Stomping 的变体与进阶
5.1 映射未落盘的 PE(避免完整性校验)
EDR 的映像完整性校验依赖磁盘文件。若映像本就不存在于磁盘(或磁盘文件无法被 EDR 读取),校验无法进行:
- 在内存中构造一个完整的 PE(DOS header + NT header + 节区 + 必要的导出表);
NtCreateSection以SEC_IMAGE创建一个 section,基于 pagefile 而非文件;NtMapViewOfSection映射到进程;- 此时 VAD 是
MEM_IMAGE,但无磁盘文件可比对,EDR 的完整性校验失败(无法校验)而非命中(不一致)。
注意:SEC_IMAGE 基于 pagefile 的 section 在某些 Windows 版本受限。可改为映射一个临时创建的文件中的 PE,加载后立即删除文件。
5.2 Stomping 已加载的 DLL(Ldr 一致)
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。










评论