EDR规避深度剖析:系统调用间接执行、ETW绕过与内核回调脱钩
从Userland到Kernel:2025年终端安全对抗的最前线|前沿攻防技术系列
一、终端对抗的新格局
EDR(Endpoint Detection and Response)已经成为企业安全的基础设施。CrowdStrike、Microsoft Defender for Endpoint、SentinelOne三巨头占据了全球90%以上的市场份额。但与此同时,EDR规避技术也进入了高度工程化的阶段——从简单的进程注入发展到了内核回调脱钩、硬件断点利用等底层对抗。
2025年,攻击者已经不再讨论"能不能绕过EDR",而是讨论"绕过EDR需要多少毫秒"。MITRE ATT&CK评估数据显示,顶级攻击团队能够在500ms内完成从初始执行到EDR供应商遥测盲区的转移。
本文将从攻防双视角,深入终端安全对抗的最前沿技术。注意:所有内容均基于公开研究和防御视角,旨在帮助安全团队理解攻击面,而非提供攻击工具。
二、EDR的检测架构——要绕过先要理解
EDR的传感器布局
现代EDR不是单一模块,而是一个多层传感器网络。理解每个传感器的能力和盲区是规避的基础:
为什么ETW是攻击者的首选目标
ETW是Windows上最强大但最不被理解的安全基础设施。几乎每个攻击行为都会产生ETW事件: ·进程创建 → Microsoft-Windows-Kernel-Process Provider → Event ID 1 ·网络连接 → Microsoft-Windows-Kernel-Network Provider → Event ID 3 ·DNS查询 → Microsoft-Windows-DNS-Client Provider → Event ID 3008 ·PowerShell执行 → Microsoft-Windows-PowerShell Provider → Event ID 4103/4104 ·.NET程序集加载 → Microsoft-Windows-DotNETRuntime Provider
攻击者的核心逻辑是:如果我能控制ETW,我就能控制EDR看到什么。以下是三种核心绕过手法。
三、系统调用间接执行(Indirect Syscall)
为什么直接Syscall已经不够了
早期EDR规避方案使用直接的syscall指令来绕过ntdll.dll的用户态Hook。但现代EDR已经进化——它们在内核层面通过ETW栈跟踪或内核回调监控syscall的返回地址。如果syscall指令来自ntdll.dll以外的内存区域(如手动分配的可执行内存),立即触发告警。
这就是间接系统调用(Indirect Syscall)诞生的背景——在ntdll.dll模块内部找一个合法的syscall指令地址,将控制流"伪装"成来自合法模块。
间接Syscall的核心实现
// 间接系统调用的核心逻辑(C伪代码)
typedef NTSTATUS (NTAPI* pNtAllocateVirtualMemory)(
HANDLE, PVOID*, ULONG_PTR, PSIZE_T, ULONG, ULONG
);
// 步骤1:从ntdll.dll中定位syscall gadget
PVOID FindSyscallGadget() {
HMODULE ntdll = GetModuleHandle(L"ntdll.dll");
PUCHAR base = (PUCHAR)ntdll;
// NtAllocateVirtualMemory在ntdll中的偏移
PUCHAR func = base + GetNtFunctionOffset("NtAllocateVirtualMemory");
// 在函数体内搜索 syscall; ret 序列(0x0F 0x05 0xC3)
for (int i = 0; i < 32; i++) {
if (func[i] == 0x0F && func[i+1] == 0x05 && func[i+2] == 0xC3) {
return &func[i];// 返回合法syscall gadget的地址
}
}
return NULL;
}
// 步骤2:设置SSN(System Service Number)和参数
NTSTATUS IndirectNtAllocateVirtualMemory(
HANDLE hProcess, PVOID* baseAddr, ULONG_PTR zeroBits,
PSIZE_T size, ULONG allocType, ULONG protect)
{
PVOID gadget = FindSyscallGadget();
DWORD ssn = GetSyscallNumber("NtAllocateVirtualMemory");
// 设置寄存器:RAX=SSN, R10=第一个参数
// 然后跳转到ntdll.dll中的合法syscall gadget
// 此时内核看到的返回地址在ntdll.dll内,EDR不会触发告警
__asm {
mov r10, rcx// 第一个参数放入R10
mov eax, ssn// 系统调用号
jmp gadget// 跳转到合法gadget(不是call,所以返回
// 地址保持在调用者的合法堆栈中)
}
}
EDR的反制:调用栈验证
面对间接syscall,EDR厂商升级了检测策略——通过内核回调(PsSetCreateProcessNotifyRoutine等)获取用户态调用栈,验证调用来源:
EDR内核回调中的调用栈验证逻辑:
捕获syscall发生时的用户态RSP
遍历调用栈帧
验证关键函数的返回地址是否落在对应模块的合法地址范围内
如果发现返回地址指向未知内存区域或栈帧结构异常 → 判定为间接调用,触发高级告警
攻击者的进一步对抗:
Stack Spoofing:伪造调用栈帧使得栈回溯结果看起来完全合法
使用ROP链在合法模块内部跳转(如ntdll中的其他gadget),使栈回溯指向合法地址
Stack Spoofing示例(简化):
__asm {
// 推送伪造的返回地址,都是合法模块内的地址
push fake_ret_addr_3// ntdll!LdrpDoDebuggerBreak+0x1c
push fake_ret_addr_2// kernel32!BaseThreadInitThunk+0x14
push fake_ret_addr_1// ntdll!RtlUserThreadStart+0x21
jmp indirect_syscall_gadget
// 当EDR回溯这个栈时,看到的是一个完全正常的调用链
}
四、ETW的完整攻防
ETW的内部架构与攻击面
ETW由三个核心组件构成,每个都是潜在的攻击面: ·Providers(提供程序):产生事件的组件,如内核和用户态应用程序 ·Controllers(控制器):管理ETW会话的组件,负责启动/停止追踪 ·Consumers(消费者):接收和处理事件的组件,如EDR的遥测收集器
攻击者主要针对Provider和Consumer之间的数据路径。核心技术是patch EtwEventWrite(ntdll中的ETW写入函数),使其在关键事件发生时不写入数据。
ETW Patch的三种技术路线
技术1:直接Patch EtwEventWrite
// 最粗暴但有效的方法——让ETW写入直接返回成功
// 找到EtvEventWrite函数,将函数开头改为 ret 0x14
PUCHAR etwFunc = GetProcAddress(ntdll, "EtwEventWrite");
DWORD oldProtect;
VirtualProtect(etwFunc, 1, PAGE_EXECUTE_READWRITE, &oldProtect);
*etwFunc = 0xC3;// 0xC3 = RET指令
VirtualProtect(etwFunc, 1, oldProtect, &oldProtect);
技术2:Hook NtTraceEvent
// EtwEventWrite最终会调用NtTraceEvent进行实际的系统调用
// 在NtTraceEvent入口处设置inline hook
// 检测到特定GUID(如PowerShell的ETW Provider GUID)时直接返回
技术3:内存补丁 EtwEventWriteFull
// Patch EtwEventWriteFull中的关键逻辑分支
// 使其在任何过滤条件下都跳过事件写入
// 更隐蔽但需要定位正确的偏移量
防御检测:
检查ntdll.dll的.text段完整性(内存 vs 磁盘对比)
监控VirtualProtect调用中涉及ntdll地址的情况
ETW本身可以监控其他ETW Provider的健康状态
五、内核回调脱钩(Kernel Callback Unhooking)
回调的工作原理
EDR在内核注册多种回调来接收系统事件。Windows内核维护着多个回调数组——每个数组是一个链表或数组结构,EDR将自己的回调函数地址插入其中。攻击者可以遍历这些数组,删除或无效化EDR的回调:
关键回调类型及其在内核中的位置:
进程通知回调
API: PsSetCreateProcessNotifyRoutine(Ex)
内部结构: PspCreateProcessNotifyRoutine[]
触发时机: 进程创建/退出时
脱钩方法: 遍历数组,将EDR的回调条目清零
线程通知回调
API: PsSetCreateThreadNotifyRoutine(Ex)
内部结构: PspCreateThreadNotifyRoutine[]
触发时机: 线程创建/退出时
模块加载回调
API: PsSetLoadImageNotifyRoutine(Ex)
内部结构: PspLoadImageNotifyRoutine[]
触发时机: DLL/EXE加载到内存时
注册表回调
API: CmRegisterCallback(Ex)
内部结构: CallbackListHead(双向链表)
触发时机: 任何注册表操作时
对象管理器回调
API: ObRegisterCallbacks
内部结构: ObpCallbackListHead
触发时机: 进程/线程句柄的创建和复制时
脱钩难度: 最高——回调结构在受保护的内核内存中
实战脱钩流程
回调脱钩的基本流程(需要驱动级权限执行):
获取内核基址:通过NtQuerySystemInformation(SystemModuleInformation)枚举内核模块
定位回调数组:通过特征码扫描或PDB符号解析,定位PspCreateProcessNotifyRoutine等全局变量
枚举回调条目:遍历数组中的每个回调结构,检查回调地址所属的驱动模块
识别EDR回调:判断回调函数地址是否属于EDR的驱动模块(如CsAgent.sys for CrowdStrike)
脱钩操作:将目标回调条目的函数指针清零,或标记为Inactive
清理痕迹:恢复被修改的内核结构的原始保护属性
防御要点:PatchGuard(内核补丁保护)会在随机间隔检查关键内核结构的完整性,包括回调数组。检测到篡改后会触发Bug Check 0x109(CRITICAL_STRUCTURE_CORRUPTION)导致蓝屏。高级攻击者通过禁用PatchGuard(需要Bootkit级别攻击)或利用Hypervisor虚拟化在更深层拦截来绕过。
六、EDR厂商的反制演进
防御方并非停滞不前。以下是2024-2025年EDR厂商的核心反制手段: ·从内核回调向硬件虚拟化迁移:Microsoft的VBS(Virtualization-Based Security)将关键安全组件移至Hypervisor层,即使攻击者控制了内核也难以触及 ·用户态行为分析:不再单纯依赖API监控,而是分析进程整体的行为图(父子进程关系、网络连接模式、内存分配模式) ·机器学习异常检测:在传感器端部署轻量级ML模型,检测异常的系统调用模式(如短时间高密度syscall = 可疑) ·内存完整性扫描:定期对比关键DLL(ntdll/kernel32)的磁盘版本和内存版本,检测Inline Hook和Patch ·内核回调的"看门狗"机制:定期验证所有注册回调都在正常运行,被脱钩立即告警
七、总结
终端安全对抗的本质是一场永无止境的军备竞赛。攻击者向底层迁移(Ring3→Ring0→Hypervisor→固件),防御方也向底层迁移。但有一个不变的原则:纵深防御。
对于防御者来说,不要指望单一的EDR能阻止所有攻击。结合应用白名单、网络分段、最小权限和运行时行为分析的多层防御体系,才能显著提高攻击成本。同时,定期进行红队演练,模拟最新的EDR规避技术来验证检测能力的有效性,是保持安全态势的必需手段。