EDR回调学习

admin 2025年2月8日01:16:21评论15 views字数 11169阅读37分13秒阅读模式

我们都知道在用户层(R3),每一个进程都有自己的执行环境,这意味着A进程崩溃了,但是是不会影响到B进程的。

用户空间和内核空间是彼此隔离的。所以用户空间的应用程序是无法访问到内核中的结构的,除非在内核中运行代码,而内核驱动程序是最简单的方式。

我们来看一下当我们尝试去打开一个文件时,操作系统到底会做什么?那么作为一个用户来说,打开一个文件只需要双击去打开即可。但是作为操作系统来说却要经过相当多的步骤。

  1. 首先用户双击文件,比如Notepad
  2. 通过Win32 API(kernel32.dll,user.dll,advapi.dll)调用文件操作相关的系统功能。
  3. Ntdll.dll是用户空间与内核空间之间的桥梁,它将API调用转换为系统调用syscall,并请求操作系统执行相应的任务。
  4. 进入内核空间,系统调用将被执行,执行服务处理请求,内核模式系统进程负责管理内存,进程和线程等等。
  5. 来到硬件抽象层,确保操作系统与不同硬件设备兼容,使文件读取和操作能够顺利执行。
  6. 最终文件从硬盘中读取,数据被传输到用户应用程序中显示。

这里我们简单去回顾一下,用户应用程序主要依赖于WinAPI,它是由多个DLL模块公开的,比如kernel32.dlluser32.dll等等。

所以打开文件的第一步是调用kernel32.dll模块中的CreateFileA函数。如果我们使用x64dbgntdll.dll中的NtCreateFile函数下一个断点,我们会发现CreateFileA函数会调用到NtCreateFile函数这里。该函数是由Ntdll公开的。

如果我们通过NtCreateFile函数来打开文件,那么程序就必须去请求内核来打开文件了。这意味着程序必须调用内核本身公开的NtCreateFile函数。我们都知道用户层的应用程序是无法访问内核层的,但是他们可以请求执行特定的任务。这里其实就是系统调用syscall了。

NtCreateFile函数的汇编代码中,主要关注如下这两行:

EDR回调学习

首先将55移动到了eax寄存器中。该值就是系统调用号。Ntdll.dll中的大部分函数都链接了一个特定的系统调用号,需要注意的是该系统调用号在各个Windows版本都是不一样的。第二行就是syscall指令。

该指令会告诉CPU从用户空间切换到内核空间,然后跳转到内核中NtCreateFile函数所在的内核地址。但是我们在想CPU怎么能够知道NtCreateFile函数在哪呢?为了找到该函数的内核地址,它需要存储在EAX寄存器中的系统调用号和SSDT。那么SSDT是什么呢?

SSDT结构是一个索引,它包含了系统调用号列表以及对应内核函数的地址。类似于如下表:

函数名称调用号内核函数地址NtCreateFile550x5ea54623NtCreateIRTimer ab 0x6bcd1576...          ...   ...

正因为有了这个表,那么当CPU从用户空间切换到内核空间时,会根据系统调用号,在SSDT表中查找所对应的内核函数地址,最后进行跳转。

那么内核接收到请求之后,它将请求驱动程序来读取存储在硬盘上的内容。那么我们在想如果更改了SSDT结构中的内核函数地址,那么就可以将代码重定向到其他的地方了。正因为如此,很多安全厂商开始修改SSDT,将系统调用重定向到他们自己的驱动程序,以便来分析调用了哪些函数,监控传递的参数和数据,检测并拦截恶意操作等等。

如下图:

很简单,用户双击1.txt文件,调用kernel32.dll模块中的CreateFile函数,调用到Ntdll.dll模块中的NtCreateFile函数,经过syscall指令将其从用户层转为内核层,通过SSDT表查找到系统调用号0x0055所对应的地址为0x123456,需要注意的是该地址是被修改过的,该地址已经被修改成反病毒软件的驱动程序了,也就是说每次去创建文件时都要经过反病毒软件来检测,最后才能真正的执行创建文件的操作。

EDR回调学习

如果操纵SSDT结构相对简单,那么操纵其他内核结构可能是非常危险的。在内核空间中,如果执行的代码存在漏洞或错误,整个内核都可能会崩溃。那么既然反病毒软件可以利用内核驱动来访问内核并修改其行为,那么攻击者也一样可以使用Rootkit内核级后门来执行类似的攻击。

为了保护其操作系统,防止被入侵,微软推出了KPP,通常称之为PatchGuardPatchGuard是一种主动的安全机制,它会定期检查多个关键的关键的Windows内核结构的状态。如果这些结构被非合法的内核代码修改,PatchGuard会触发一个致命的系统错误从而强制计算机重启。

随着PatchGuard安全机制的发布,反病毒软件不再从内核中挂钩SSDT了。那么这样的话意味着就无法检测了吗?

为了解决这个问题,并允许安全产品重新监控系统,微软在操作系统中新添加了一些功能,这些功能主要依赖于回调对象的机制。

这样以来EDR就能够动态的监控系统上的活动,而无需直接修改SSDT。该回调对象作为一种新的内核监控机制,允许内核驱动在特定事件发生时收到通知,从而避免直接修改SSDT

微软提供了几种常见的回调机制:

  1. PsSetCreateProcessNotifyRoutineEx该回调机制用于进程创建/终止回调,当系统创建或终止进程时,驱动会收到通知。
  2. PsSetCreateThreadNotifyRoutine该回调机制用于线程创建/终止回调,该回调时线程级别的监控,例如检测恶意代码的线程注入。
  3. PsSetLoadImageNotifyRoutine该回调用于加载/卸载驱动回调,监控恶意驱动或DLL的加载行为。
  4. CmRegisterCallbackEx该回调用于监控注册表的修改。
  5. FltRegisterFilter该回调用于监控文件的访问和修改行为。

接下来我们将来开发驱动程序。关于如何开发驱动程序,我之前在文章中已经写过了,大家可以在纷传找一下。

我们来解释一下如下代码,首先DriverEntry函数好比我们的main函数,这里需要两个参数分别是DriverObject以及RegistryPath

DriverObject指向驱动程序对象的指针,操作系统使用它来维护驱动程序的信息。RegistryPath指向Unicode字符串的指针,表示驱动程序的注册路径。

首先这里使用到了UNREFERENCED_PARAMETER宏,该宏用于标记未初始化的参数,防止编译器发出警告。既然DriverEntry函数是驱动加载的时候调用,那么驱动卸载时我们需要为其注册一个Unload例程。DriverObject->DriverUnload = UnloadDriver;指向Unload例程,当驱动卸载时就会调用。

至于DbgPrint函数是用于调试输出的。

#include <ntddk.h>extern "C" VOID UnloadDriver(PDRIVER_OBJECT DriverObject){    UNREFERENCED_PARAMETER(DriverObject);    DbgPrint("UnloadDriver executed: Driver is being unloaded.n");}extern "C" NTSTATUS DriverEntry(PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath){    UNREFERENCED_PARAMETER(RegistryPath);    DriverObject->DriverUnload = UnloadDriver;    DbgPrint("DriverEntry executed: Driver has been loaded successfully.n");    return STATUS_SUCCESS;}
EDR回调学习

那么驱动程序可以用来注册所谓的内核回调的函数。内核回调的基本思想是,每次在系统上执行特定操作时,内核都会通知注册回调的驱动程序正在执行的操作。

如果要注册这样的内核回调,有如下一些回调函数:

  1. PsSetCreateProcessNotifyRoutineEx该回调机制用于进程创建/终止回调,当系统创建或终止进程时,驱动会收到通知。
  2. PsSetCreateThreadNotifyRoutine该回调机制用于线程创建/终止回调,该回调时线程级别的监控,例如检测恶意代码的线程注入。
  3. PsSetLoadImageNotifyRoutine该回调用于加载/卸载驱动回调,监控恶意驱动或DLL的加载行为。
  4. CmRegisterCallbackEx该回调用于监控注册表的修改。
  5. FltRegisterFilter该回调用于监控文件的访问和修改行为。

我们拿PsSetCreateProcessNotifyRoutine回调函数来举一个例子。如下图:

EDR回调学习
我们来看看PsSetCreateProcessNotifyRoutine回调机制:
NTSTATUS PsSetCreateProcessNotifyRoutine(    PCREATE_PROCESS_NOTIFY_ROUTINE NotifyRoutine, //指向回调函数的指针 内核会在进程创建/停止时调用它    BOOLEAN Remove //FALSE表示注册回调 TRUE表示取消注册回调);
我们来看看回调函数的定义:
typedef VOID (*PCREATE_PROCESS_NOTIFY_ROUTINE)(    HANDLE ParentId,     HANDLE ProcessId,     BOOLEAN Create);

该函数需要定义三个参数,分别是PPID父进程ID,ID进程ID以及Create 通过Create来判断是进程创建还是终止。

所以我们需要去创建一个回调函数:

extern "C" void CreateProcessNotifyRoutine(HANDLE ppid, HANDLE pid, BOOLEAN create){  UNREFERENCED_PARAMETER(ppid);    //判断create是否为TRUE   if (create){        PEPROCESS process = NULL;        PUNICODE_STRING processName = NULL;      //通过pid获取该进程的内核对象EPROCESS        PsLookupProcessByProcessId(pid, &process);      //获取该进程的完整路径        SeLocateProcessImageName(process, &processName);//输出进程PID以及进程名称        DbgPrint("MyDumbEDR: %d (%wZ) launched.n", pid, processName);    }    else{        DbgPrint("MyDumbEDR: %d got killed.n", pid);    }}
注册回调机制:
PsSetCreateProcessNotifyRoutine(CreateProcessNotifyRoutine, FALSE);
完整代码:
#include <Ntifs.h>#include <ntddk.h>#include <wdf.h>UNICODE_STRING DEVICE_NAME = RTL_CONSTANT_STRING(L"\Device\Lhackeredr");UNICODE_STRING SYM_LINK = RTL_CONSTANT_STRING(L"\??\Lhackeredr");extern "C" void CreateProcessNotifyRoutine(HANDLE ppid, HANDLE pid, BOOLEAN create) {    UNREFERENCED_PARAMETER(ppid);    if (create) {        PEPROCESS process = NULL;        PUNICODE_STRING processName = NULL;        if (NT_SUCCESS(PsLookupProcessByProcessId(pid, &process))) {            if (NT_SUCCESS(SeLocateProcessImageName(process, &processName)) && processName) {  // 防止 NULL 访问                DbgPrint("MyDumbEDR: %d (%wZ) launched.n", pid, processName);            }            else {                DbgPrint("MyDumbEDR: %d launched, but failed to get process name.n", pid);            }            ObDereferenceObject(process);  // 释放 EPROCESS        }    }    else {        DbgPrint("MyDumbEDR: %d got killed.n", pid);    }}extern "C" void UnloadMyDumbEDR(_In_ PDRIVER_OBJECT DriverObject) {    DbgPrint("MyDumbEDR: Unloading routine calledn");    // **正确移除进程回调**    PsSetCreateProcessNotifyRoutine(CreateProcessNotifyRoutine, TRUE);    IoDeleteSymbolicLink(&SYM_LINK);    IoDeleteDevice(DriverObject->DeviceObject);}extern "C" NTSTATUS DriverEntry(_In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING RegistryPath) {    UNREFERENCED_PARAMETER(RegistryPath);    DbgPrint("MyDumbEDR: Initializing the drivern");    NTSTATUS status;    PDEVICE_OBJECT DeviceObject = NULL;    status = IoCreateDevice(        DriverObject,        0,        &DEVICE_NAME,        FILE_DEVICE_UNKNOWN,        FILE_DEVICE_SECURE_OPEN,  // 设备特性修正        FALSE,        &DeviceObject    );    if (!NT_SUCCESS(status)) {        DbgPrint("MyDumbEDR: Device creation failed with status: 0x%Xn", status);        return status;    }    status = IoCreateSymbolicLink(&SYM_LINK, &DEVICE_NAME);    if (!NT_SUCCESS(status)) {        DbgPrint("MyDumbEDR: Symlink creation failed with status: 0x%Xn", status);        IoDeleteDevice(DeviceObject);        return status;    }    // **注册进程回调**    status = PsSetCreateProcessNotifyRoutine(CreateProcessNotifyRoutine, FALSE);    if (!NT_SUCCESS(status)) {        DbgPrint("MyDumbEDR: Failed to register process notify routine. Status: 0x%Xn", status);        IoDeleteSymbolicLink(&SYM_LINK);        IoDeleteDevice(DeviceObject);        return status;    }    DriverObject->DriverUnload = UnloadMyDumbEDR;    return STATUS_SUCCESS;}
可以看到当我们新启动一个进程时,在DbgView中查看到已输出该进程的PID以及进程名称。
EDR回调学习
当停止该进程时也会输出该进程的PID
EDR回调学习

如上我们只是获取到了当进程启动时的PID以及进程名称,那么如果我们想要阻止进程创建的话,我们就需要使用到PsSetCreateProcessNotifyRoutineEx该回调机制了。

PsSetCreateProcessNotifyRoutineEx函数是Windows10 1809版本引入的增强版PsSetCreateProcessNotifyRoutine。它提供了更多进程相关的信息,比如PEPROCESS指针和IMAGE_FILE_NAME

函数原型如下:

NTSTATUS PsSetCreateProcessNotifyRoutineEx(    PCREATE_PROCESS_NOTIFY_ROUTINE_EX NotifyRoutine, //指向进程创建/终止回调函数的指针    BOOLEAN Remove //FALSE表示注册回调 TRUE表示取消回调);
回调函数定义如下:
typedef VOID (*PCREATE_PROCESS_NOTIFY_ROUTINE_EX)(    _Inout_ PEPROCESS Process, //进程对象的指针    _In_ HANDLE ProcessId, //进程的PID    _Inout_opt_ PPS_CREATE_NOTIFY_INFO CreateInfo //进程创建的相关信息 如果为NULL 则表示进程正在终止。);
CreateInfo != NULL时,它包含了进程创建时的详细信息。它包含了进程创建时的详细信息。
typedef struct _PS_CREATE_NOTIFY_INFO {    SIZE_T Size;    union {        ULONG Flags;        struct {            ULONG FileOpenNameAvailable : 1;            ULONG IsSubsystemProcess : 1;            ULONG Reserved : 30;        };    };    HANDLE ParentProcessId; //父进程PID    CLIENT_ID CreatingThreadId; //创建进程的线程ID    struct _FILE_OBJECT *FileObject; //文件对象指针    PCUNICODE_STRING ImageFileName; //进程的完整路径    PCUNICODE_STRING CommandLine; //命令行参数    NTSTATUS CreationStatus; //如果在回调中修改此字段 可拦截进程创建} PS_CREATE_NOTIFY_INFO, *PPS_CREATE_NOTIFY_INFO;
如下回调函数实现:
extern "C" void CreateProcessNotifyRoutine(PEPROCESS process, HANDLE pid, PPS_CREATE_NOTIFY_INFO createInfo) {    UNREFERENCED_PARAMETER(process);    UNREFERENCED_PARAMETER(pid);  //判断CreateInfo结构是否为空    if (createInfo != NULL) {      //判断命令行参数中如果包含了mimikatz则阻止该进程创建        if (wcsstr(createInfo->CommandLine->Buffer, L"mimikatz") != NULL) {            DbgPrint("MyDumbEDR: Process (%ws) denied.n", createInfo->CommandLine->Buffer);            createInfo->CreationStatus = STATUS_ACCESS_DENIED;        }    }}
如下代码:
#include <Ntifs.h>#include <ntddk.h>#include <wdf.h>UNICODE_STRING DEVICE_NAME = RTL_CONSTANT_STRING(L"\Device\Lhackeredr");UNICODE_STRING SYM_LINK = RTL_CONSTANT_STRING(L"\??\Lhackeredr");void CreateProcessNotifyRoutine(PEPROCESS process, HANDLE pid, PPS_CREATE_NOTIFY_INFO createInfo) {    UNREFERENCED_PARAMETER(process);    UNREFERENCED_PARAMETER(pid);    // 防止 NULL 访问    if (createInfo && createInfo->CommandLine && createInfo->CommandLine->Buffer) {        if (wcsstr(createInfo->CommandLine->Buffer, L"mimikatz") != NULL) {            DbgPrint("MyDumbEDR: Process (%ws) denied.n", createInfo->CommandLine->Buffer);            createInfo->CreationStatus = STATUS_ACCESS_DENIED;  // 阻止进程创建        }    }}void UnloadMyDumbEDR(_In_ PDRIVER_OBJECT DriverObject) {    DbgPrint("MyDumbEDR: Unloading routine calledn");    // **正确移除进程回调**    PsSetCreateProcessNotifyRoutineEx(CreateProcessNotifyRoutine, TRUE);    IoDeleteSymbolicLink(&SYM_LINK);    IoDeleteDevice(DriverObject->DeviceObject);}extern "C" NTSTATUS DriverEntry(_In_ PDRIVER_OBJECT DriverObject, _In_ PUNICODE_STRING RegistryPath) {    UNREFERENCED_PARAMETER(RegistryPath);    DbgPrint("MyDumbEDR: Initializing the drivern");    NTSTATUS status;    PDEVICE_OBJECT DeviceObject = NULL;    status = IoCreateDevice(        DriverObject,        0,        &DEVICE_NAME,        FILE_DEVICE_UNKNOWN,        FILE_DEVICE_SECURE_OPEN,        FALSE,        &DeviceObject    );    if (!NT_SUCCESS(status)) {        DbgPrint("MyDumbEDR: Device creation failed with status: 0x%Xn", status);        return status;    }    status = IoCreateSymbolicLink(&SYM_LINK, &DEVICE_NAME);    if (!NT_SUCCESS(status)) {        DbgPrint("MyDumbEDR: Symlink creation failed with status: 0x%Xn", status);        IoDeleteDevice(DeviceObject);        return status;    }    // **注册进程回调**    status = PsSetCreateProcessNotifyRoutineEx(CreateProcessNotifyRoutine, FALSE);    if (!NT_SUCCESS(status)) {        DbgPrint("MyDumbEDR: Failed to register process notify routine. Status: 0x%Xn", status);        IoDeleteSymbolicLink(&SYM_LINK);        IoDeleteDevice(DeviceObject);        return status;    }    DriverObject->DriverUnload = UnloadMyDumbEDR;    return STATUS_SUCCESS;}
Windows内核允许注册特定的回调函数来监视或拦截系统事件,比如进程创建,线程创建,图像加载等等。这些回调函数存储在内核的特定数组中,每个数组都有一定的最大回调数。
函数回调对应数组名称最大可注册回调数PsSetCreateProcessNotifyRoutinePspCreateProcessNotifyRoutine64PsSetCreateThreadNotifyRoutinePspCreateThreadNotifyRoutine64PsSetLoadImageNotifyRoutinePspLoadImageNotifyRoutine8CmRegisterCallbackCmpCallBackVector100
我们可以使用windbg来查看数组的实际内容。
dq nt!PspCreateProcessNotifyRoutine
EDR回调学习

所以每当进程启动时,内核就会读取PspCreateProcessNotifyRoutine数组,并对其该数组中的每一个指针发送有关正在创建的进程的通知。那么如果你可以覆盖他们或删除指针,这可以导致EDR失效。

我们上述简单开发的阻止新的进程创建,只是去判断命令行中是否有mimikatz字段。如果我们将其更改为其他的名称,例如mimikanotepad.exe,这将绕过该检查。

由于安全性和稳定性的考虑,几乎所有的EDR都会依赖于用户代理来协调整个EDR解决方案。这个用户空间代理至少会执行两项关键的任务。

  1. 对即将在系统上运行的二进制文件进行静态分析,通过分析进程可执行文件的哈希,签名,代码模式等信息,来判断是否存在恶意行为。
  2. 向进程内注入自定义DLL来监控API的调用。

如下图:

EDR回调学习

内核驱动程序通过内核回调机制来接收有关系统上正在执行的特定操作的通知,然后将其转发给开发大多数检测逻辑的代理。

那么对于实现一个简单的EDR来说是有点复杂的。EDR可以依赖于三个组件,分别是内核驱动程序将接收有关正在创建的进程的通知,用户空间代理将静态分析二进制文件以及在每个正在创建的进程中注入自定义的DLL

如下图:

EDR回调学习
最后如果您看爽了。

原文始发于微信公众号(Relay学安全):EDR回调学习

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2025年2月8日01:16:21
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   EDR回调学习https://cn-sec.com/archives/3703813.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息