我们都知道在用户层(R3
),每一个进程都有自己的执行环境,这意味着A
进程崩溃了,但是是不会影响到B
进程的。
用户空间和内核空间是彼此隔离的。所以用户空间的应用程序是无法访问到内核中的结构的,除非在内核中运行代码,而内核驱动程序是最简单的方式。
我们来看一下当我们尝试去打开一个文件时,操作系统到底会做什么?那么作为一个用户来说,打开一个文件只需要双击去打开即可。但是作为操作系统来说却要经过相当多的步骤。
- 首先用户双击文件,比如
Notepad
。 - 通过
Win32 API
(kernel32.dll,user.dll,advapi.dll
)调用文件操作相关的系统功能。 Ntdll.dll
是用户空间与内核空间之间的桥梁,它将API
调用转换为系统调用syscall
,并请求操作系统执行相应的任务。- 进入内核空间,系统调用将被执行,执行服务处理请求,内核模式系统进程负责管理内存,进程和线程等等。
- 来到硬件抽象层,确保操作系统与不同硬件设备兼容,使文件读取和操作能够顺利执行。
- 最终文件从硬盘中读取,数据被传输到用户应用程序中显示。
这里我们简单去回顾一下,用户应用程序主要依赖于WinAPI
,它是由多个DLL
模块公开的,比如kernel32.dll
user32.dll
等等。
所以打开文件的第一步是调用kernel32.dll
模块中的CreateFileA
函数。如果我们使用x64dbg
在ntdll.dll
中的NtCreateFile
函数下一个断点,我们会发现CreateFileA
函数会调用到NtCreateFile
函数这里。该函数是由Ntdll
公开的。
如果我们通过NtCreateFile
函数来打开文件,那么程序就必须去请求内核来打开文件了。这意味着程序必须调用内核本身公开的NtCreateFile
函数。我们都知道用户层的应用程序是无法访问内核层的,但是他们可以请求执行特定的任务。这里其实就是系统调用syscall
了。
在NtCreateFile
函数的汇编代码中,主要关注如下这两行:
首先将55
移动到了eax
寄存器中。该值就是系统调用号。Ntdll.dll
中的大部分函数都链接了一个特定的系统调用号,需要注意的是该系统调用号在各个Windows
版本都是不一样的。第二行就是syscall
指令。
该指令会告诉CPU
从用户空间切换到内核空间,然后跳转到内核中NtCreateFile
函数所在的内核地址。但是我们在想CPU
怎么能够知道NtCreateFile
函数在哪呢?为了找到该函数的内核地址,它需要存储在EAX
寄存器中的系统调用号和SSDT
。那么SSDT
是什么呢?
SSDT
结构是一个索引,它包含了系统调用号列表以及对应内核函数的地址。类似于如下表:
函数名称调用号内核函数地址
NtCreateFile550x5ea54623
NtCreateIRTimer ab 0x6bcd1576
... ... ...
正因为有了这个表,那么当CPU
从用户空间切换到内核空间时,会根据系统调用号,在SSDT
表中查找所对应的内核函数地址,最后进行跳转。
那么内核接收到请求之后,它将请求驱动程序来读取存储在硬盘上的内容。那么我们在想如果更改了SSDT
结构中的内核函数地址,那么就可以将代码重定向到其他的地方了。正因为如此,很多安全厂商开始修改SSDT
,将系统调用重定向到他们自己的驱动程序,以便来分析调用了哪些函数,监控传递的参数和数据,检测并拦截恶意操作等等。
如下图:
很简单,用户双击1.txt
文件,调用kernel32.dll
模块中的CreateFile
函数,调用到Ntdll.dll
模块中的NtCreateFile
函数,经过syscall
指令将其从用户层转为内核层,通过SSDT
表查找到系统调用号0x0055
所对应的地址为0x123456
,需要注意的是该地址是被修改过的,该地址已经被修改成反病毒软件的驱动程序了,也就是说每次去创建文件时都要经过反病毒软件来检测,最后才能真正的执行创建文件的操作。
如果操纵SSDT
结构相对简单,那么操纵其他内核结构可能是非常危险的。在内核空间中,如果执行的代码存在漏洞或错误,整个内核都可能会崩溃。那么既然反病毒软件可以利用内核驱动来访问内核并修改其行为,那么攻击者也一样可以使用Rootkit
内核级后门来执行类似的攻击。
为了保护其操作系统,防止被入侵,微软推出了KPP
,通常称之为PatchGuard
。PatchGuard
是一种主动的安全机制,它会定期检查多个关键的关键的Windows
内核结构的状态。如果这些结构被非合法的内核代码修改,PatchGuard
会触发一个致命的系统错误从而强制计算机重启。
随着PatchGuard
安全机制的发布,反病毒软件不再从内核中挂钩SSDT
了。那么这样的话意味着就无法检测了吗?
为了解决这个问题,并允许安全产品重新监控系统,微软在操作系统中新添加了一些功能,这些功能主要依赖于回调对象的机制。
这样以来EDR
就能够动态的监控系统上的活动,而无需直接修改SSDT
。该回调对象作为一种新的内核监控机制,允许内核驱动在特定事件发生时收到通知,从而避免直接修改SSDT
。
微软提供了几种常见的回调机制:
PsSetCreateProcessNotifyRoutineEx
该回调机制用于进程创建/终止回调,当系统创建或终止进程时,驱动会收到通知。PsSetCreateThreadNotifyRoutine
该回调机制用于线程创建/终止回调,该回调时线程级别的监控,例如检测恶意代码的线程注入。PsSetLoadImageNotifyRoutine
该回调用于加载/卸载驱动回调,监控恶意驱动或DLL
的加载行为。CmRegisterCallbackEx
该回调用于监控注册表的修改。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;
}
那么驱动程序可以用来注册所谓的内核回调的函数。内核回调的基本思想是,每次在系统上执行特定操作时,内核都会通知注册回调的驱动程序正在执行的操作。
如果要注册这样的内核回调,有如下一些回调函数:
PsSetCreateProcessNotifyRoutineEx
该回调机制用于进程创建/终止回调,当系统创建或终止进程时,驱动会收到通知。PsSetCreateThreadNotifyRoutine
该回调机制用于线程创建/终止回调,该回调时线程级别的监控,例如检测恶意代码的线程注入。PsSetLoadImageNotifyRoutine
该回调用于加载/卸载驱动回调,监控恶意驱动或DLL
的加载行为。CmRegisterCallbackEx
该回调用于监控注册表的修改。FltRegisterFilter
该回调用于监控文件的访问和修改行为。
我们拿PsSetCreateProcessNotifyRoutine
回调函数来举一个例子。如下图:
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
以及进程名称。PID
。如上我们只是获取到了当进程启动时的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
内核允许注册特定的回调函数来监视或拦截系统事件,比如进程创建,线程创建,图像加载等等。这些回调函数存储在内核的特定数组中,每个数组都有一定的最大回调数。函数回调对应数组名称最大可注册回调数
PsSetCreateProcessNotifyRoutinePspCreateProcessNotifyRoutine64
PsSetCreateThreadNotifyRoutinePspCreateThreadNotifyRoutine64
PsSetLoadImageNotifyRoutinePspLoadImageNotifyRoutine8
CmRegisterCallbackCmpCallBackVector100
windbg
来查看数组的实际内容。dq nt!PspCreateProcessNotifyRoutine
所以每当进程启动时,内核就会读取PspCreateProcessNotifyRoutine
数组,并对其该数组中的每一个指针发送有关正在创建的进程的通知。那么如果你可以覆盖他们或删除指针,这可以导致EDR
失效。
我们上述简单开发的阻止新的进程创建,只是去判断命令行中是否有mimikatz
字段。如果我们将其更改为其他的名称,例如mimikanotepad.exe
,这将绕过该检查。
由于安全性和稳定性的考虑,几乎所有的EDR
都会依赖于用户代理来协调整个EDR
解决方案。这个用户空间代理至少会执行两项关键的任务。
- 对即将在系统上运行的二进制文件进行静态分析,通过分析进程可执行文件的哈希,签名,代码模式等信息,来判断是否存在恶意行为。
- 向进程内注入自定义
DLL
来监控API
的调用。
如下图:
内核驱动程序通过内核回调机制来接收有关系统上正在执行的特定操作的通知,然后将其转发给开发大多数检测逻辑的代理。
那么对于实现一个简单的EDR
来说是有点复杂的。EDR
可以依赖于三个组件,分别是内核驱动程序将接收有关正在创建的进程的通知,用户空间代理将静态分析二进制文件以及在每个正在创建的进程中注入自定义的DLL
。
如下图:
原文始发于微信公众号(Relay学安全):EDR回调学习
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论