https://www.vergiliusproject.com/
系统通知型回调(Notify Callbacks) 系统通知型回调 是 Windows 内核提供的一类机制,允许内核模式驱动程序注册回调函数,以便在系统关键事件 (如进程/线程创建、模块加载、句柄操作、注册表修改等)发生时收到通知。
常见的系统通知型回调 有下面几种类型:
事件类型
注册函数
回调函数类型
进程创建/退出
PsSetCreateProcessNotifyRoutine(Ex)
PCREATE_PROCESS_NOTIFY_ROUTINE(_EX)
线程创建/退出
PsSetCreateThreadNotifyRoutine(Ex)
PCREATE_THREAD_NOTIFY_ROUTINE
模块加载(DLL、EXE、SYS)
PsSetLoadImageNotifyRoutine(Ex)
PLOAD_IMAGE_NOTIFY_ROUTINE
注册表修改
CmRegisterCallbackEx
PEX_CALLBACK_FUNCTION
对象句柄操作
ObRegisterCallbacks
POB_PRE_OPERATION_CALLBACK
文件系统操作
FltRegisterFilter
+ FltStartFiltering
FLT_PREOP_CALLBACK_ROUTINE
等
注册表配置单元加载(Hive)
CmRegisterCallbackEx
PEX_CALLBACK_FUNCTION
进程回调 进程回调是 Windows 内核为驱动提供的重要机制之一,它允许驱动在进程创建和退出时收到系统通知 ,以实现行为监控、日志记录、安全检测和控制等功能。
Windows 提供两种进程回调机制:
PCREATE_PROCESS_NOTIFY_ROUTINE
(基础版)
PCREATE_PROCESS_NOTIFY_ROUTINE_EX
(扩展版,推荐)
基础回调注册 PCREATE_PROCESS_NOTIFY_ROUTINE
是 Windows 早期版本提供的进程回调函数类型,功能相对有限,仅提供进程的 PID、父进程 ID 和创建/销毁事件标志。
1 2 3 4 5 typedef VOID (*PCREATE_PROCESS_NOTIFY_ROUTINE) ( _In_ HANDLE ParentId, _In_ HANDLE ProcessId, _In_ BOOLEAN Create ) ;
PCREATE_PROCESS_NOTIFY_ROUTINE
类型的回调需要通过 PsSetCreateProcessNotifyRoutine
函数注册 和移除 。
1 2 3 4 5 6 NTKERNELAPI NTSTATUS PsSetCreateProcessNotifyRoutine ( _In_ PCREATE_PROCESS_NOTIFY_ROUTINE NotifyRoutine, _In_ BOOLEAN Remove ) ;
示例代码如下:
1 2 3 4 5 6 7 8 9 10 11 VOID BasicProcessNotify (HANDLE ParentId, HANDLE ProcessId, BOOLEAN Create) { if (Create) { DbgPrint("创建进程:PID=%p,父PID=%p\n" , ProcessId, ParentId); } else { DbgPrint("退出进程:PID=%p\n" , ProcessId); } } PsSetCreateProcessNotifyRoutine(BasicProcessNotify, FALSE); PsSetCreateProcessNotifyRoutine(BasicProcessNotify, TRUE);
如果我们注册了回调函数,但是在驱动卸载的时候没有移除回调函数,则由于实现回调函数的驱动被卸载了,因此会导致内核访问无效内存地址(PAGE_FAULT_IN_NONPAGED_AREA
),引发蓝屏崩溃。
因此我们通常在驱动卸载函数中移除回调函数。而 PsSetCreateProcessNotifyRoutine
本身支持注册与移除(通过 Remove=TRUE
)。
1 2 3 4 5 6 7 8 9 NTSTATUS DriverUnload (PDRIVER_OBJECT DriverObject) { NTSTATUS status = PsSetCreateProcessNotifyRoutineEx(MyProcessCallback, TRUE); if (!NT_SUCCESS(status)) { KdPrint(("移除回调失败: 0x%X\n" , status)); } return STATUS_SUCCESS; }
扩展回调注册 PCREATE_PROCESS_NOTIFY_ROUTINE_EX
是对基础回调的扩展,提供更丰富的进程创建上下文,包括 EPROCESS
指针、映像路径、命令行、映像对象等。该回调函数类型定义如下:
1 2 3 4 5 typedef VOID (*PCREATE_PROCESS_NOTIFY_ROUTINE_EX) ( _Inout_ PEPROCESS Process, _In_ HANDLE ProcessId, _Inout_opt_ PPS_CREATE_NOTIFY_INFO CreateInfo ) ;
PCREATE_PROCESS_NOTIFY_ROUTINE_EX
函数没有专门的参数来表示进程是被创建,但是 CreateInfo
参数只有进程创建的时候才非空,因此我们可以通过 CreateInfo
是否为 NULL
来确定进程是销毁还是创建。
当进程正在创建 时,CreateInfo != NULL
;
当进程正在退出 时,CreateInfo == NULL
。
PCREATE_PROCESS_NOTIFY_ROUTINE_EX
不仅提供了进程 ID,还提供了进程的 EPROCESS
结构以及存放额外的信息的结构体 PPS_CREATE_NOTIFY_INFO
,该结构体定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 typedef struct _PS_CREATE_NOTIFY_INFO { _In_ SIZE_T Size; union { _In_ ULONG Flags; struct { _In_ ULONG FileOpenNameAvailable : 1 ; _In_ ULONG IsSubsystemProcess : 1 ; _In_ ULONG Reserved : 30 ; }; }; _In_ HANDLE ParentProcessId; _In_ CLIENT_ID CreatingThreadId; _Inout_ struct _FILE_OBJECT *FileObject ; _In_ PCUNICODE_STRING ImageFileName; _In_opt_ PCUNICODE_STRING CommandLine; _Inout_ NTSTATUS CreationStatus; } PS_CREATE_NOTIFY_INFO, *PPS_CREATE_NOTIFY_INFO;
PCREATE_PROCESS_NOTIFY_ROUTINE_EX
回调函数是通过 PsSetCreateProcessNotifyRoutineEx
函数注册或删除的,注意不要与 PsSetCreateProcessNotifyRoutine
混用。
1 2 3 4 5 6 NTKERNELAPI NTSTATUS PsSetCreateProcessNotifyRoutineEx ( _In_ PCREATE_PROCESS_NOTIFY_ROUTINE_EX NotifyRoutine, _In_ BOOLEAN Remove ) ;
PsSetCreateProcessNotifyRoutineEx
可能因未签名驱动被拒绝注册回调,返回错误码:STATUS_ACCESS_DENIED (0xC0000022)
。
通过分析发现是 MmVerifyCallbackFunction
函数未通过回调函数的校验返回 FALSE
导致的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 NTKERNELAPI NTSTATUS PsSetCreateProcessNotifyRoutineEx ( _In_ PCREATE_PROCESS_NOTIFY_ROUTINE_EX NotifyRoutine, _In_ BOOLEAN Remove ) { return PspSetCreateProcessNotifyRoutine(NotifyRoutine, Remove, TRUE); } NTKERNELAPI NTSTATUS PspSetCreateProcessNotifyRoutine ( PVOID NotifyRoutine, BOOLEAN Remove, BOOLEAN IsExtended ) { if (IsExtended && !MmVerifyCallbackFunction(NotifyRoutine)) { return STATUS_ACCESS_DENIED; } }
MmVerifyCallbackFunction
函数会调用 MiLookupDataTableEntry
获取回调函数 NotifyRoutine
对应的 PKLDR_DATA_TABLE_ENTRY
结构,然后根据其中的 Flags
字段是否设置了 0x20 标志位确定是否返回 TRUE
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 NTKERNELAPI BOOLEAN MmVerifyCallbackFunction ( PVOID NotifyRoutine ) { DataTableEntry = MiLookupDataTableEntry(NotifyRoutine, TRUE); if (DataTableEntry) { IsValidCallback = (DataTableEntry->Flags & 0x20 ) != 0 ; } return IsValidCallback; }
MiLookupDataTableEntry
会遍历所有已加载模块,判断传入的回调函数在哪个模块的地址范围内,返回该模块对应的 KLDR_DATA_TABLE_ENTRY
结构。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 PKLDR_DATA_TABLE_ENTRY MiLookupDataTableEntry ( IN PVOID AddressWithinSection, IN ULONG ResourceHeld ) { PKTHREAD CurrentThread; PKLDR_DATA_TABLE_ENTRY DataTableEntry; PKLDR_DATA_TABLE_ENTRY FoundEntry; PLIST_ENTRY NextEntry; PAGED_CODE(); FoundEntry = NULL ; if (!ResourceHeld) { CurrentThread = KeGetCurrentThread(); KeEnterCriticalRegionThread(CurrentThread); ExAcquireResourceSharedLite(&PsLoadedModuleResource, TRUE); } else { CurrentThread = NULL ; } NextEntry = PsLoadedModuleList.Flink; ASSERT(NextEntry != NULL ); do { DataTableEntry = CONTAINING_RECORD(NextEntry, KLDR_DATA_TABLE_ENTRY, InLoadOrderLinks); if (AddressWithinSection >= DataTableEntry->DllBase && AddressWithinSection < (PVOID)((PUCHAR)DataTableEntry->DllBase + DataTableEntry->SizeOfImage)) { FoundEntry = DataTableEntry; break ; } NextEntry = NextEntry->Flink; } while (NextEntry != &PsLoadedModuleList); if (CurrentThread != NULL ) { ExReleaseResourceLite(&PsLoadedModuleResource); KeLeaveCriticalRegionThread(CurrentThread); } return FoundEntry; }
实际上上述检查只是判断驱动是否经过微软官方签名。而绕过方法也很简单,我们只需要修改驱动对应的 KLDR_DATA_TABLE_ENTRY
结构的 Flags
字段,使其 0x20 置位即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 typedef struct _KLDR_DATA_TABLE_ENTRY { LIST_ENTRY InLoadOrderLinks; ULONG __Undefined1; ULONG __Undefined2; ULONG __Undefined3; ULONG NonPagedDebugInfo; ULONG DllBase; ULONG EntryPoint; ULONG SizeOfImage; UNICODE_STRING FullDllName; UNICODE_STRING BaseDllName; ULONG Flags; } KLDR_DATA_TABLE_ENTRY, *PKLDR_DATA_TABLE_ENTRY; NTSTATUS DriverEntry (PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath) { PKLDR_DATA_TABLE_ENTRY DataTableEntry; DataTableEntry = (PKLDR_DATA_TABLE_ENTRY)DriverObject->DriverSection; DataTableEntry->Flags |= 0x20 ; DriverObject->DriverUnload = DriverUnload; return STATUS_SUCCESS; }
进程回调分析 回调注册 无论是否是扩展回调,进程回调最终都是通过 PspSetCreateProcessNotifyRoutine
函数注册的,而这个函数的第三个参数用来表示是否是扩展回调。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 NTKERNELAPI NTSTATUS PsSetCreateProcessNotifyRoutineEx ( _In_ PCREATE_PROCESS_NOTIFY_ROUTINE_EX NotifyRoutine, _In_ BOOLEAN Remove ) { return PspSetCreateProcessNotifyRoutine(NotifyRoutine, Remove, TRUE); } NTKERNELAPI NTSTATUS PsSetCreateProcessNotifyRoutine ( _In_ PCREATE_PROCESS_NOTIFY_ROUTINE NotifyRoutine, _In_ BOOLEAN Remove ) { return PspSetCreateProcessNotifyRoutine(NotifyRoutine, Remove, TRUE); } NTKERNELAPI NTSTATUS PspSetCreateProcessNotifyRoutine ( PVOID NotifyRoutine, BOOLEAN Remove, BOOLEAN IsExtended ) ;
PspSetCreateProcessNotifyRoutine
函数代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 NTSTATUS PspSetCreateProcessNotifyRoutine ( _In_ PVOID NotifyRoutine, _In_ BOOLEAN Remove, _In_ BOOLEAN IsExtended ) { PEX_CALLBACK_ROUTINE_BLOCK CallbackBlock; PPSP_NOTIFY_ENTRY NotifyEntry; ULONG i; if (Remove) { PKTHREAD CurrentThread = KeGetCurrentThread(); KeEnterCriticalRegionThread(CurrentThread); for (i = 0 ; i < PSP_MAX_CREATE_PROCESS_NOTIFY; i++) { NotifyEntry->CallbackBlock = &PspCreateProcessNotifyRoutine[i]; CallbackBlock = ExReferenceCallBackBlock(NotifyEntry); if (CallbackBlock == NULL ) continue ; if ((PVOID)ExGetCallBackBlockRoutine(CallbackBlock) == NotifyRoutine) { if ((IsExtended && !NotifyEntry->IsExtended) || (!IsExtended && NotifyEntry->IsExtended)) { ExDereferenceCallBackBlock((PEX_CALLBACK)&NotifyEntry->CallbackBlock, CallbackBlock); continue ; } if (ExCompareExchangeCallBack( (PEX_CALLBACK)&NotifyEntry->CallbackBlock, NULL , CallbackBlock)) { if (IsExtended) InterlockedDecrement(&PspCreateProcessNotifyRoutineExCount); else InterlockedDecrement(&PspCreateProcessNotifyRoutineCount); ExDereferenceCallBackBlock((PEX_CALLBACK)&NotifyEntry->CallbackBlock, CallbackBlock); KeLeaveCriticalRegionThread(CurrentThread); ExWaitForCallBacks(CallbackBlock); ExFreeCallBack(CallbackBlock); return STATUS_SUCCESS; } } ExDereferenceCallBackBlock((PEX_CALLBACK)&NotifyEntry->CallbackBlock, CallbackBlock); } KeLeaveCriticalRegionThread(CurrentThread); return STATUS_PROCEDURE_NOT_FOUND; } if (IsExtended && !MmVerifyCallbackFunction(NotifyRoutine)) return STATUS_ACCESS_DENIED; CallbackBlock = ExAllocateCallBack((PEX_CALLBACK_FUNCTION)NotifyRoutine, NULL ); if (!CallbackBlock) return STATUS_INSUFFICIENT_RESOURCES; for (i = 0 ; i < PSP_MAX_CREATE_PROCESS_NOTIFY; i++) { NotifyEntry->CallbackBlock = &PspCreateProcessNotifyRoutine[i]; if (ExCompareExchangeCallBack( NotifyEntry->CallbackBlock, CallbackBlock, NULL )) { NotifyEntry->IsExtended = IsExtended; if (IsExtended) { InterlockedIncrement(&PspCreateProcessNotifyRoutineExCount); if (!(PspNotifyEnableMask & PSP_NOTIFY_PROCESS_EX)) { InterlockedBitTestAndSet(&PspNotifyEnableMask, PSP_NOTIFY_PROCESS_EX_BIT); } } else { InterlockedIncrement(&PspCreateProcessNotifyRoutineCount); if (!(PspNotifyEnableMask & PSP_NOTIFY_PROCESS)) { InterlockedBitTestAndSet(&PspNotifyEnableMask, PSP_NOTIFY_PROCESS_BIT); } } return STATUS_SUCCESS; } } ExFreeCallBack(CallbackBlock); return STATUS_INVALID_PARAMETER; }
对于注销流程 ,该函数的处理逻辑如下:
进入线程临界区,防止回调数组竞争;
遍历回调槽 PspCreateProcessNotifyRoutine[i]
:
如果 NotifyRoutine
匹配,并且回调类型也匹配(EX/普通):
使用 ExCompareExchangeCallBack()
原子替换为 NULL
;
更新计数(PspCreateProcessNotifyRoutineCount
或 ExCount
);
等待所有正在执行的回调返回;
调用 ExFreeCallBack()
释放回调资源;
返回 STATUS_SUCCESS
。
否则,释放引用,继续查找;
如果未找到匹配回调,返回 STATUS_PROCEDURE_NOT_FOUND
。
对于注册流程 ,该函数的处理逻辑如下:
调用 MmVerifyCallbackFunction
校验函数地址安全性(如签名);
失败则返回 STATUS_ACCESS_DENIED
。
使用 ExAllocateCallBack()
创建 CallbackBlock
;
失败返回 STATUS_INSUFFICIENT_RESOURCES
。
遍历回调槽,尝试原子插入:
插入成功则:
设置 IsExtended
标志;
更新对应计数;
设置 PspNotifyEnableMask
中对应 bit(普通/EX);
返回 STATUS_SUCCESS
。
所有槽都被占用时,释放分配的回调,返回 STATUS_INVALID_PARAMETER
。
回调槽 PspCreateProcessNotifyRoutine
大小为 PSP_MAX_CREATE_PROCESS_NOTIFY(64)
,也就是说我们最多注册 64 个进程回调函数(包括扩展回调和非扩展回调)。
回调函数会按照先后循序放入回调槽,越早注册的函数越靠前,后续也会越早调用,方便拦截后续的回调函数。
回调调用 对于进程创建事件 ,进程回调函数会在 PspInsertThread
函数中被调用,这是因为第一个线程创建的时候一定是进程创建的时候。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 if (Process->ActiveThreads == 1 ) { EtwTraceProcess(Process, ETW_EVENT_PROCESS_CREATE); NTSTATUS CreationStatus = STATUS_SUCCESS; BOOLEAN HasExNotify = (PspNotifyEnableMask & PSP_NOTIFY_PROCESS_EX) != 0 ; if (PspNotifyEnableMask & (PSP_NOTIFY_PROCESS | PSP_NOTIFY_PROCESS_EX)) { PFILE_OBJECT FileObject = NULL ; BOOLEAN NeedDereference = FALSE; PS_CREATE_NOTIFY_INFO NotifyInfo; if (HasExNotify) { NotifyInfo.Size = sizeof (NotifyInfo); NotifyInfo.Flags = 0 ; NotifyInfo.ParentProcessId = Process->InheritedFromUniqueProcessId; NotifyInfo.CreatingThreadId = CONTAINING_RECORD( KeGetCurrentThread(), _ETHREAD, Tcb)->Cid; NotifyInfo.CreationStatus = STATUS_SUCCESS; if (CreateProcessContext && CreateProcessContext->FileObject) { FileObject = CreateProcessContext->FileObject; } else { NeedDereference = TRUE; PsReferenceProcessFilePointer(Process, &FileObject); } NotifyInfo.FileObject = FileObject; if (CreateProcessContext && (CreateProcessContext->Flags & 0x20 )) { NotifyInfo.ImageFileName = &CreateProcessContext->ImageFileName; NotifyInfo.FileOpenNameAvailable = TRUE; } else { NotifyInfo.ImageFileName = &FileObject->FileName; } if (CreateProcessContext && CreateProcessContext->CommandLine) { NotifyInfo.CommandLine = CreateProcessContext->CommandLine + sizeof (UNICODE_STRING); } else { NotifyInfo.CommandLine = NULL ; } } EX_CALLBACK_ROUTINE_BLOCK* CallbackBlock; for (int i = 0 ; i < MAX_CREATE_PROCESS_NOTIFY; ++i) { CallbackBlock = ExReferenceCallBackBlock(&PspCreateProcessNotifyRoutine[i]); if (!CallbackBlock) continue ; PVOID NotifyRoutine = ExGetCallBackBlockRoutine(CallbackBlock); if (CallbackBlock->Context) { if (HasExNotify) { ((PCREATE_PROCESS_NOTIFY_ROUTINE_EX)NotifyRoutine)( Process, Process->UniqueProcessId, &NotifyInfo); } } else { ((PCREATE_PROCESS_NOTIFY_ROUTINE)NotifyRoutine)( Process->InheritedFromUniqueProcessId, Process->UniqueProcessId, TRUE); } ExDereferenceCallBackBlock(CallbackBlock); if (HasExNotify && !NT_SUCCESS(NotifyInfo.CreationStatus)) { PsTerminateProcess(Process, NotifyInfo.CreationStatus); break ; } } if (NeedDereference) ObfDereferenceObject(FileObject); } }
PspNotifyEnableMask
是一个全局的位掩码变量 ,用于控制系统通知类回调(如进程、线程、镜像加载等) 的启用状态。它的每一位分别表示某一类回调是否启用。
对于进程回调:
bit 1 :即代码中的 PSP_NOTIFY_PROCESS_BIT
,用于控制普通的 PsSetCreateProcessNotifyRoutine
回调是否启用。
bit 2 :即代码中的 PSP_NOTIFY_PROCESS_EX_BIT
,用于控制扩展的 PsSetCreateProcessNotifyRoutineEx
回调是否启用。
因此我们可以短暂清空相关标志位来规避一些敏感行为的监控。
如果是扩展回调 ,则设置扩展回调的结构体中 PS_CREATE_NOTIFY_INFO
中的 CreationStatus
可以阻止进程创建。
NotifyInfo
在调用扩展回调函数的过程中是共用的,因此我们可以通过修改 NotifyInfo
结构体中的内容从而影响到后续扩展回调函数的判断;同理,Process
结构体也可以被修改。
对于进程退出事件 则是在 PspExitProcess
函数中调用的回调函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 if (PspNotifyEnableMask & (PSP_NOTIFY_PROCESS | PSP_NOTIFY_PROCESS_EX)){ ULONG i; PEX_CALLBACK_ROUTINE_BLOCK CallbackBlock; PCREATE_PROCESS_NOTIFY_ROUTINE CallbackFunction; PPSP_NOTIFY_ENTRY NotifyEntry = PspCreateProcessNotifyRoutine; BOOLEAN ExtendedEnabled = (PspNotifyEnableMask & PSP_NOTIFY_PROCESS_EX) != 0 ; for (i = 0 ; i < PSP_MAX_CREATE_PROCESS_NOTIFY; i++, NotifyEntry++) { CallbackBlock = ExReferenceCallBackBlock((PEX_CALLBACK)&NotifyEntry->CallbackBlock); if (CallbackBlock == NULL ) continue ; if (NotifyEntry->IsExtended && !ExtendedEnabled) { ExDereferenceCallBackBlock((PEX_CALLBACK)&NotifyEntry->CallbackBlock, CallbackBlock); continue ; } CallbackFunction = (PCREATE_PROCESS_NOTIFY_ROUTINE)ExGetCallBackBlockRoutine(CallbackBlock); CallbackFunction( NotifyEntry->IsExtended ? Process->InheritedFromUniqueProcessId : Process->UniqueProcessId, Process->UniqueProcessId, FALSE ); ExDereferenceCallBackBlock((PEX_CALLBACK)&NotifyEntry->CallbackBlock, CallbackBlock); } }
线程回调 线程回调是 Windows 内核提供的重要机制之一,允许驱动在线程创建与销毁事件发生时接收通知 ,以实现监控、日志记录、安全检测等功能。Windows 提供了线程回调函数 PCREATE_THREAD_NOTIFY_ROUTINE
。
1 2 3 4 5 typedef VOID (*PCREATE_THREAD_NOTIFY_ROUTINE) ( _In_ HANDLE ProcessId, _In_ HANDLE ThreadId, _In_ BOOLEAN Create ) ;
基础回调注册 与进程回调不同,线程回调函数 PCREATE_THREAD_NOTIFY_ROUTINE
的注册和移除函数是两个不同的函数:
注册线程回调 :
1 2 3 4 5 NTKERNELAPI NTSTATUS PsSetCreateThreadNotifyRoutine ( _In_ PCREATE_THREAD_NOTIFY_ROUTINE NotifyRoutine ) ;
移除线程回调 :
1 2 3 4 5 NTKERNELAPI NTSTATUS PsRemoveCreateThreadNotifyRoutine ( _In_ PCREATE_THREAD_NOTIFY_ROUTINE NotifyRoutine ) ;
示例代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 VOID MyThreadNotifyCallback ( HANDLE ProcessId, HANDLE ThreadId, BOOLEAN Create ) { if (Create) { DbgPrint("线程创建: PID=%p, TID=%p\n" , ProcessId, ThreadId); } else { DbgPrint("线程销毁: PID=%p, TID=%p\n" , ProcessId, ThreadId); } } NTSTATUS DriverEntry (PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath) { PsSetCreateThreadNotifyRoutine(MyThreadNotifyCallback); DriverObject->DriverUnload = DriverUnload; return STATUS_SUCCESS; } VOID DriverUnload (PDRIVER_OBJECT DriverObject) { PsRemoveCreateThreadNotifyRoutine(MyThreadNotifyCallback); }
扩展回调注册 PsSetCreateThreadNotifyRoutineEx
是 Windows 10 引入的增强线程回调接口,允许驱动程序注册用于监听系统范围内线程创建和销毁事件 的回调函数。
该函数比旧的 PsSetCreateThreadNotifyRoutine
更灵活,支持控制回调类型(如是否在被创建线程的上下文中调用),并允许特权驱动在系统范围内对线程生命周期进行精细监控。
PsSetCreateThreadNotifyRoutineEx
函数声明如下:
1 2 3 4 5 6 NTKERNELAPI NTSTATUS PsSetCreateThreadNotifyRoutineEx ( _In_ PSCREATETHREADNOTIFYTYPE NotifyType, _In_ PVOID NotifyInformation ) ;
PsSetCreateThreadNotifyRoutineEx
注册的回调函数依旧使用 PsRemoveCreateThreadNotifyRoutine
卸载。
其中 PSCREATETHREADNOTIFYTYPE
目前支持下面两种值:
1 2 3 4 typedef enum _PSCREATETHREADNOTIFYTYPE { PsCreateThreadNotifyNonSystem = 0 , PsCreateThreadNotifySubsystems = 1 } PSCREATETHREADNOTIFYTYPE;
线程回调分析 回调注册 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 NTSTATUS PsSetCreateThreadNotifyRoutine ( _In_ PCREATE_THREAD_NOTIFY_ROUTINE NotifyRoutine ) { PEX_CALLBACK_ROUTINE_BLOCK CallbackBlock; PEX_CALLBACK CallbackSlot; ULONG Index; PAGED_CODE(); CallbackBlock = ExAllocateCallBack((PEX_CALLBACK_FUNCTION)NotifyRoutine, NULL ); if (CallbackBlock == NULL ) return STATUS_INSUFFICIENT_RESOURCES; for (Index = 0 ; Index < PSP_MAX_CREATE_THREAD_NOTIFY; Index++) { CallbackSlot = &PspCreateThreadNotifyRoutine[Index]; if (ExCompareExchangeCallBack( CallbackSlot, CallbackBlock, NULL )) { InterlockedIncrement(&PspCreateThreadNotifyRoutineCount); if (!(PspNotifyEnableMask & PSP_NOTIFY_THREAD)) { InterlockedBitTestAndSet(&PspNotifyEnableMask, PSP_NOTIFY_THREAD_BIT); } return STATUS_SUCCESS; } } ExFreeCallBack(CallbackBlock); return STATUS_INSUFFICIENT_RESOURCES; }
回调移除 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 NTSTATUS PsRemoveCreateThreadNotifyRoutine ( _In_ PCREATE_THREAD_NOTIFY_ROUTINE NotifyRoutine ) { PKTHREAD CurrentThread; PEX_CALLBACK_ROUTINE_BLOCK CallbackBlock; ULONG i; PAGED_CODE(); CurrentThread = KeGetCurrentThread(); KeEnterCriticalRegionThread(CurrentThread); for (i = 0 ; i < PSP_MAX_CREATE_THREAD_NOTIFY; i++) { CallbackBlock = ExReferenceCallBackBlock(&PspCreateThreadNotifyRoutine[i]); if (CallbackBlock == NULL ) continue ; if ((PCREATE_THREAD_NOTIFY_ROUTINE)ExGetCallBackBlockRoutine(CallbackBlock) == NotifyRoutine) { if (ExCompareExchangeCallBack( &PspCreateThreadNotifyRoutine[i], NULL , CallbackBlock)) { InterlockedDecrement(&PspCreateThreadNotifyRoutineCount); ExDereferenceCallBackBlock(&PspCreateThreadNotifyRoutine[i], CallbackBlock); KeLeaveCriticalRegionThread(CurrentThread); ExWaitForCallBacks(CallbackBlock); ExFreeCallBack(CallbackBlock); return STATUS_SUCCESS; } } ExDereferenceCallBackBlock(&PspCreateThreadNotifyRoutine[i], CallbackBlock); } KeLeaveCriticalRegionThread(CurrentThread); return STATUS_PROCEDURE_NOT_FOUND; }
回调调用 对于线程创建事件 ,线程回调函数在 PspInsertThread
函数中被调用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 if (PspCreateThreadNotifyRoutineCount != 0 ) { ULONG Index; PEX_CALLBACK_ROUTINE_BLOCK Callback; PCREATE_THREAD_NOTIFY_ROUTINE NotifyRoutine; for (Index = 0 ; Index < PSP_MAX_CREATE_THREAD_NOTIFY; Index++) { Callback = ExReferenceCallBackBlock(&PspCreateThreadNotifyRoutine[Index]); if (Callback != NULL ) { NotifyRoutine = (PCREATE_THREAD_NOTIFY_ROUTINE)ExGetCallBackBlockRoutine(Callback); NotifyRoutine(CONTAINING_RECORD(Object->Tcb.Process, _EPROCESS, Pcb)->UniqueProcessId, Thread->Cid.UniqueThread, TRUE); ExDereferenceCallBackBlock(&PspCreateThreadNotifyRoutine[Index], Callback); } } }
对于线程退出事件 ,线程回调函数在 PspExitThread
函数中被调用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 if (PspNotifyEnableMask & PSP_NOTIFY_THREAD){ PEX_CALLBACK_ROUTINE_BLOCK CallbackBlock; PCREATE_THREAD_NOTIFY_ROUTINE NotifyRoutine; ULONG Index; for (Index = 0 ; Index < PSP_MAX_CREATE_THREAD_NOTIFY; Index++) { CallbackBlock = ExReferenceCallBackBlock(&PspCreateThreadNotifyRoutine[Index]); if (CallbackBlock) { NotifyRoutine = (PCREATE_THREAD_NOTIFY_ROUTINE)ExGetCallBackBlockRoutine(CallbackBlock); NotifyRoutine( CONTAINING_RECORD(CurThread->Tcb.Process, _EPROCESS, Pcb)->UniqueProcessId, CurThread->Cid.UniqueThread, FALSE ); ExDereferenceCallBackBlock(&PspCreateThreadNotifyRoutine[Index], CallbackBlock); } } }
模块回调 模块回调是 Windows 内核提供的通知机制之一,允许驱动程序在任何模块(包括 EXE、DLL、SYS)加载到进程地址空间时收到通知 ,以便实现监控、日志记录、DLL 注入检测、签名验证等功能。
Windows 提供的模块回调函数类型为:
1 2 3 4 5 typedef VOID (*PLOAD_IMAGE_NOTIFY_ROUTINE) ( _In_opt_ PUNICODE_STRING FullImageName, _In_ HANDLE ProcessId, _In_ PIMAGE_INFO ImageInfo ) ;
其中 PIMAGE_INFO
结构体描述了加载的映像的地址、大小和加载类型等信息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 typedef struct _IMAGE_INFO { union { ULONG Properties; struct { ULONG ImageAddressingMode : 8 ; ULONG SystemModeImage : 1 ; ULONG ImageMappedToAllPids : 1 ; ULONG ExtendedInfoPresent : 1 ; ULONG MachineTypeMismatch : 1 ; ULONG ImageSignatureLevel : 4 ; ULONG ImageSignatureType : 3 ; ULONG ImagePartialMap : 1 ; ULONG Reserved : 12 ; }; }; PVOID ImageBase; ULONG ImageSelector; SIZE_T ImageSize; ULONG ImageSectionNumber; } IMAGE_INFO, *PIMAGE_INFO;
基础回调注册 模块回调函数 PLOAD_IMAGE_NOTIFY_ROUTINE
的注册和移除函数是两个不同的函数:
注册模块回调 :
1 2 3 4 5 NTKERNELAPI NTSTATUS PsSetLoadImageNotifyRoutine ( _In_ PLOAD_IMAGE_NOTIFY_ROUTINE NotifyRoutine ) ;
移除模块回调 :
1 2 3 4 5 NTKERNELAPI NTSTATUS PsRemoveLoadImageNotifyRoutine ( _In_ PLOAD_IMAGE_NOTIFY_ROUTINE NotifyRoutine ) ;
示例代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 VOID ImageLoadCallback ( PUNICODE_STRING FullImageName, HANDLE ProcessId, PIMAGE_INFO ImageInfo ) { if (FullImageName && ProcessId != 0 ) { DbgPrint("进程 %p 加载映像:%wZ,基址=%p,大小=0x%Ix\n" , ProcessId, FullImageName, ImageInfo->ImageBase, ImageInfo->ImageSize); } } NTSTATUS DriverEntry (PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath) { PsSetLoadImageNotifyRoutine(ImageLoadCallback); DriverObject->DriverUnload = DriverUnload; return STATUS_SUCCESS; } VOID DriverUnload (PDRIVER_OBJECT DriverObject) { PsRemoveLoadImageNotifyRoutine(ImageLoadCallback); }
模块回调的开关位于 PspNotifyEnableMask
的最低位。
1 #define PSP_NOTIFY_IMAGE 1
扩展回调注册 PsSetLoadImageNotifyRoutineEx
是 Win10 引入的模块回调注册函数,相比于旧的 PsSetLoadImageNotifyRoutine
,此版本支持传入 Flags 参数 ,以便控制回调行为,如是否通知跨架构(x86/x64)映像等。
1 2 3 4 NTSTATUS PsSetLoadImageNotifyRoutineEx ( _In_ PLOAD_IMAGE_NOTIFY_ROUTINE NotifyRoutine, _In_ ULONG_PTR Flags ) ;
其中 Flags
目前官方仅定义支持 PS_IMAGE_NOTIFY_CONFLICTING_ARCHITECTURE(0x1)
,表示启用跨架构通知(例如:在 x64 系统上也通知加载 x86 模块)。
对象回调(Object Callbacks) 早期防护软件为了拦截对进程、线程等敏感对象的句柄创建/复制,普遍使用 SSDT/内核钩子等侵入式技术,极易引发系统不稳定。自 Windows Vista SP1 / Windows Server 2008 起,微软在 Object Manager 上提供了 对象回调机制(Object Callbacks) 。
在Windows内核中,对象回调机制(Object Callbacks) 是一种安全和监控机制,允许内核模式驱动程序注册特定的回调函数,以监视或拦截对某些内核对象(例如进程、线程、文件等)的操作。
对象回调使用 回调注册卸载 对象回调机制提供了专门的内核函数,用于驱动程序注册和卸载回调:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 NTKERNELAPI NTSTATUS ObRegisterCallbacks ( _In_ POB_CALLBACK_REGISTRATION CallbackRegistration, _Outptr_ PVOID *RegistrationHandle ) ;NTKERNELAPI VOID ObUnRegisterCallbacks ( _In_ PVOID RegistrationHandle ) ;
ObRegisterCallbacks
同样要求驱动签名,因此依旧需要进程扩展回调的绕过方法。
其中 OB_CALLBACK_REGISTRATION
结构用于描述一次对象访问回调注册请求,包含回调的元信息、目标对象类型、回调函数数组等。该结构体定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 typedef struct _OB_CALLBACK_REGISTRATION { USHORT Version; USHORT OperationRegistrationCount; UNICODE_STRING Altitude; PVOID RegistrationContext; OB_OPERATION_REGISTRATION* OperationRegistration; } OB_CALLBACK_REGISTRATION, *POB_CALLBACK_REGISTRATION;
Version
: 结构体版本号,必须设置为 OB_FLT_REGISTRATION_VERSION(0x100)
,但是为了防止未来的兼容性问题,建议使用 ObGetFilterVersion
函数动态获取 。
Altitude
:微软在内核回调机制(尤其是对象管理器回调 和文件过滤驱动 )中用于排序执行优先级 的机制,本质上是一个全局唯一的字符串表示的“高度值” 。为了同时支持优先级排序 和阅读与区分 ,要求格式为 <数字>.<驱动/产品标识符>
,例如 320000.MyDriverName
。
RegistrationContext
:定义上下文指针,会作为参数 RegistrationContext
传递给每次触发的 PreOperation
/ PostOperation
回调函数。
我们注册的回调函数位于 OperationRegistration
指向的 OB_OPERATION_REGISTRATION
结构体数组,其中 OperationRegistrationCount
表示数组中元素的数量。OB_OPERATION_REGISTRATION
结构体定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 typedef struct _OB_OPERATION_REGISTRATION { _In_ POBJECT_TYPE *ObjectType; _In_ OB_OPERATION Operations; _In_ POB_PRE_OPERATION_CALLBACK PreOperation; _In_ POB_POST_OPERATION_CALLBACK PostOperation; } OB_OPERATION_REGISTRATION, *POB_OPERATION_REGISTRATION;
这个结构体描述了我们要注册的回调函数 ,以及回调函数监控的对象 和事件 。
首先监控对象 通过 ObjectType
字段描述,这个字段描述要拦截的对象种类 ,如进程、线程、注册表项等。我们需要将其填充为一个系统导出的全局的对象类型描述符指针。
对象名称
符号
说明
进程
PsProcessType
所有 EPROCESS
对象
线程
PsThreadType
所有 ETHREAD
对象
注册表键
CmKeyObjectType
所有注册表项对象(KEY_OBJECT
)
事件
ExEventObjectType
事件对象(非用户事件)
信号量等
ExSemaphoreObjectType
同步对象(部分可见)
监控事件 通过 Operations
字段描述,用于指定拦截的操作类型 (是句柄的创建,还是句柄的复制)。该字段是一个位标志(flags),值可以为以下之一或组合:
1 2 #define OB_OPERATION_HANDLE_CREATE 0x00000001 #define OB_OPERATION_HANDLE_DUPLICATE 0x00000002
OB_OPERATION_HANDLE_CREATE
:当系统执行创建一个新对象句柄(handle) 时触发此事件,也就是从用户态请求访问某个对象 ,比如进程、线程、注册表项等,进入内核后准备返回一个句柄。
OB_OPERATION_HANDLE_DUPLICATE
:当系统执行复制一个已存在的对象句柄 时触发此事件,也就是一个进程希望将已有句柄复制到另一个进程的句柄表中。对应的 API 为 ZwDuplicateObject
。
OB_OPERATION_REGISTRATION
中提供两种类型的回调函数 PreOperation
和 PostOperation
。其中:
PreOperation
:在句柄创建/复制发生之前 调用,允许回调函数修改访问权限,直接阻止访问行为。
PostOperation
:在句柄创建/复制完成之后 调用,不再允许修改访问权限。
注意这两个回调函数至少需要填写一个。
回调函数注册的示例代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 #include <ntddk.h> #include <ntstrsafe.h> typedef struct _KLDR_DATA_TABLE_ENTRY { LIST_ENTRY InLoadOrderLinks; ULONG __Undefined1; ULONG __Undefined2; ULONG __Undefined3; ULONG NonPagedDebugInfo; ULONG DllBase; ULONG EntryPoint; ULONG SizeOfImage; UNICODE_STRING FullDllName; UNICODE_STRING BaseDllName; ULONG Flags; } KLDR_DATA_TABLE_ENTRY, *PKLDR_DATA_TABLE_ENTRY; static PVOID g_ObCallbackHandle = NULL ;VOID DriverUnload (PDRIVER_OBJECT DriverObject) { UNREFERENCED_PARAMETER(DriverObject); if (g_ObCallbackHandle) { ObUnRegisterCallbacks(g_ObCallbackHandle); g_ObCallbackHandle = NULL ; DbgPrint("Ob callback unregistered.\n" ); } DbgPrint("Driver unloaded.\n" ); } NTSTATUS DriverEntry (PDRIVER_OBJECT DriverObject, PUNICODE_STRING RegistryPath) { UNREFERENCED_PARAMETER(RegistryPath); PKLDR_DATA_TABLE_ENTRY DataTableEntry; DataTableEntry = (PKLDR_DATA_TABLE_ENTRY)DriverObject->DriverSection; DataTableEntry->Flags |= 0x20 ; OB_CALLBACK_REGISTRATION callbackReg = { 0 }; OB_OPERATION_REGISTRATION opReg = { 0 }; UNICODE_STRING altitude = RTL_CONSTANT_STRING(L"320000.MyProcessProtector" ); DriverObject->DriverUnload = DriverUnload; opReg.ObjectType = *PsProcessType; opReg.Operations = OB_OPERATION_HANDLE_CREATE; opReg.PreOperation = PreProcessHandleCreateCallback; opReg.PostOperation = PostProcessHandleCreateCallback; callbackReg.Version = ObGetFilterVersion(); callbackReg.OperationRegistrationCount = 1 ; callbackReg.Altitude = altitude; callbackReg.RegistrationContext = NULL ; callbackReg.OperationRegistration = &opReg; NTSTATUS status = ObRegisterCallbacks(&callbackReg, &g_ObCallbackHandle); if (!NT_SUCCESS(status)) { DbgPrint("ObRegisterCallbacks failed: 0x%08X\n" , status); return status; } DbgPrint("Ob callback registered successfully.\n" ); return STATUS_SUCCESS; }
预操作回调函数 预操作回调函数 是 Windows 内核在执行某些对象访问操作(如进程、线程、注册表键的句柄创建或复制)之前,调用驱动提供的拦截处理函数 。预操作回调函数类型声明如下,该函数会在,在对象的句柄尚未创建之前 调用。
1 2 3 4 5 6 7 8 9 10 11 typedef OB_PREOP_CALLBACK_STATUS (*POB_PRE_OPERATION_CALLBACK) ( _In_ PVOID RegistrationContext, _Inout_ POB_PRE_OPERATION_INFORMATION OperationInformation ) ;
预操作回调函数只能返回 OB_PREOP_SUCCESS
。
1 2 3 typedef enum _OB_PREOP_CALLBACK_STATUS { OB_PREOP_SUCCESS } OB_PREOP_CALLBACK_STATUS, *POB_PREOP_CALLBACK_STATUS;
其中 RegistrationContext
参数来自于注册回调函数时 OB_CALLBACK_REGISTRATION
结构体的 RegistrationContext
成员,用于传递用户自定义的参数。
另外 OperationInformation
参数存放了发生回调时保存的信息,该参数对应的 OB_PRE_OPERATION_INFORMATION
结构体定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 typedef struct _OB_PRE_OPERATION_INFORMATION { _In_ OB_OPERATION Operation; union { _In_ ULONG Flags; struct { _In_ ULONG KernelHandle : 1 ; _In_ ULONG Reserved : 31 ; }; }; _In_ PVOID Object; _In_ POBJECT_TYPE ObjectType; _Out_ PVOID CallContext; _In_ POB_PRE_OPERATION_PARAMETERS Parameters; } OB_PRE_OPERATION_INFORMATION, *POB_PRE_OPERATION_INFORMATION;
OB_PRE_OPERATION_INFORMATION
结构体主要描述了事件类型 和内核对象 相关信息。
由于事件分为 CREATE
和 DUPLICATE
两种,因此事件参数字段 Parameters
是一个联合体 OB_PRE_OPERATION_PARAMETERS
。
1 2 3 4 5 6 7 8 9 typedef union _OB_PRE_OPERATION_PARAMETERS { OB_PRE_CREATE_HANDLE_INFORMATION CreateHandleInformation; OB_PRE_DUPLICATE_HANDLE_INFORMATION DuplicateHandleInformation; } OB_PRE_OPERATION_PARAMETERS, *POB_PRE_OPERATION_PARAMETERS;
CREATE
事件 的参数是通过 OB_PRE_CREATE_HANDLE_INFORMATION
结构体描述,该结构体定义如下:
1 2 3 4 5 6 7 8 9 typedef struct _OB_PRE_CREATE_HANDLE_INFORMATION { _Inout_ ACCESS_MASK DesiredAccess; _In_ ACCESS_MASK OriginalDesiredAccess; } OB_PRE_CREATE_HANDLE_INFORMATION, *POB_PRE_CREATE_HANDLE_INFORMATION;
由于是在创建句柄前的回调,因此此时句柄还未创建,也就不会在参数中传入。
这里参数主要传入的是用户打开句柄时的请求权限, DesiredAccess
和 OriginalDesiredAccess
的值传入的是一样的,但是我们只能通过修改 DesiredAccess
来设置最终授予用户的访问权限 。
Windows 内核之所以在 OB_PRE_CREATE_HANDLE_INFORMATION
中同时提供 OriginalDesiredAccess
(只读)和 DesiredAccess
(可修改),是为了 “保护请求意图” 与 “允许干预授权” 分离 —— 这是一种安全审计 + 控制分层设计理念。
例如下面这个示例代码可以给指定的进程句柄降权,从而达到了保护进程的目的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 static HANDLE g_ProtectedPid = (HANDLE)1234 ;OB_PREOP_CALLBACK_STATUS PreProcessHandleCreateCallback ( _Inout_ POB_PRE_OPERATION_INFORMATION OperationInformation ) { if (OperationInformation->Operation != OB_OPERATION_HANDLE_CREATE) return OB_PREOP_SUCCESS; if (OperationInformation->ObjectType != *PsProcessType) return OB_PREOP_SUCCESS; PEPROCESS targetProcess = (PEPROCESS)OperationInformation->Object; HANDLE pid = PsGetProcessId(targetProcess); if (pid == g_ProtectedPid) { ACCESS_MASK* pAccess = &OperationInformation->Parameters ->CreateHandleInformation.DesiredAccess; *pAccess &= ~(PROCESS_TERMINATE | PROCESS_VM_WRITE | PROCESS_VM_READ); DbgPrint("Protected PID %u: access 0x%08X denied sensitive permissions\n" , (ULONG)(ULONG_PTR)pid, *pAccess); } return OB_PREOP_SUCCESS; }
另外我们还可以通过调整 Altitude
让我们的对象回调函数注册到进程保护模块的对象回调函数之后,这样我们就可以恢复被降权句柄的权限。
DUPLICATE
事件 由于是进行句柄复制 ,因此会同时传递句柄权限 ,源进程 和目标进程 。
1 2 3 4 5 6 7 8 9 10 11 typedef struct _OB_PRE_DUPLICATE_HANDLE_INFORMATION { _Inout_ ACCESS_MASK DesiredAccess; _In_ ACCESS_MASK OriginalDesiredAccess; _In_ PVOID SourceProcess; _In_ PVOID TargetProcess; } OB_PRE_DUPLICATE_HANDLE_INFORMATION, *POB_PRE_DUPLICATE_HANDLE_INFORMATION;
这里句柄权限同样分为 DesiredAccess
和 OriginalDesiredAccess
两种。
后操作回调函数 后操作回调函数,是在对象访问操作(如创建或复制句柄)完成之后 ,由 Windows 内核自动调用的一个函数,用于通知驱动“这次访问操作已经完成”,供驱动执行观察、记录、收尾处理 等任务。该函数类型 POB_POST_OPERATION_CALLBACK
定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 typedef VOID (*POB_POST_OPERATION_CALLBACK) ( _In_ PVOID RegistrationContext, _In_ POB_POST_OPERATION_INFORMATION OperationInformation ) ;
其中 OB_POST_OPERATION_INFORMATION
与前面的 OB_PRE_OPERATION_INFORMATION
结构完全一致,只不过 Parameters
的类型由 POB_PRE_OPERATION_PARAMETERS
变成了 POB_POST_OPERATION_PARAMETERS
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 typedef struct _OB_POST_OPERATION_INFORMATION { _In_ OB_OPERATION Operation; union { _In_ ULONG Flags; struct { _In_ ULONG KernelHandle:1 ; _In_ ULONG Reserved:31 ; }; }; _In_ PVOID Object; _In_ POBJECT_TYPE ObjectType; _In_ PVOID CallContext; _In_ NTSTATUS ReturnStatus; _In_ POB_POST_OPERATION_PARAMETERS Parameters; } OB_POST_OPERATION_INFORMATION,*POB_POST_OPERATION_INFORMATION;
而由于后操作回调函数不修改权限,因此 OB_POST_OPERATION_PARAMETERS
中只有 GrantedAccess
,并且对于 DUPLICATE
事件也不提供双方的进程对象地址。
1 2 3 4 5 6 7 8 9 10 11 12 typedef struct _OB_POST_CREATE_HANDLE_INFORMATION { _In_ ACCESS_MASK GrantedAccess; } OB_POST_CREATE_HANDLE_INFORMATION, *POB_POST_CREATE_HANDLE_INFORMATION; typedef struct _OB_POST_DUPLICATE_HANDLE_INFORMATION { _In_ ACCESS_MASK GrantedAccess; } OB_POST_DUPLICATE_HANDLE_INFORMATION, * POB_POST_DUPLICATE_HANDLE_INFORMATION; typedef union _OB_POST_OPERATION_PARAMETERS { _In_ OB_POST_CREATE_HANDLE_INFORMATION CreateHandleInformation; _In_ OB_POST_DUPLICATE_HANDLE_INFORMATION DuplicateHandleInformation; } OB_POST_OPERATION_PARAMETERS, *POB_POST_OPERATION_PARAMETERS;
回调相关结构 对象回调的核心原理是在系统内核中维护一个“回调注册链表 ”,驱动程序通过提供的内核API注册自身的回调函数,并将其插入到对应对象类型(如进程或线程)的回调链表中。
每当内核执行对象操作(如打开句柄、复制句柄)时,都会主动遍历并调用这些注册的回调函数,进行拦截和控制。
OBJECT_TYPE _OBJECT_TYPE
结构体描述了 某一种对象类型的整体特征 ,例如进程(PsProcessType
)、线程(PsThreadType
)、文件(IoFileObjectType
)等。每个类型在内核中都有一个唯一的 _OBJECT_TYPE
实例,供该类型的所有对象共享。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 struct _OBJECT_TYPE { struct _LIST_ENTRY TypeList ; struct _UNICODE_STRING Name ; VOID* DefaultObject; UCHAR Index; ULONG TotalNumberOfObjects; ULONG TotalNumberOfHandles; ULONG HighWaterNumberOfObjects; ULONG HighWaterNumberOfHandles; struct _OBJECT_TYPE_INITIALIZER TypeInfo ; struct _EX_PUSH_LOCK TypeLock ; ULONG Key; struct _LIST_ENTRY CallbackList ; };
其中 CallbackList
是一个链表,存放我们注册的对象回调函数的存储结构 OB_CALLBACK_ENTRY
。
OBJECT_TYPE_INITIALIZER _OBJECT_TYPE_INITIALIZER
定义了创建一个对象类型时需要提供的一组 初始化属性与行为接口 。它是 _OBJECT_TYPE
中 TypeInfo
字段的类型。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 struct _OBJECT_TYPE_INITIALIZER { USHORT Length; union { UCHAR ObjectTypeFlags; struct { UCHAR CaseInsensitive : 1 ; UCHAR UnnamedObjectsOnly : 1 ; UCHAR UseDefaultObject : 1 ; UCHAR SecurityRequired : 1 ; UCHAR MaintainHandleCount : 1 ; UCHAR MaintainTypeList : 1 ; UCHAR SupportsObjectCallbacks : 1 ; UCHAR Reserved : 1 ; }; }; ULONG ObjectTypeCode; ULONG InvalidAttributes; struct _GENERIC_MAPPING GenericMapping ; ULONG ValidAccessMask; ULONG RetainAccess; enum _POOL_TYPE PoolType ; ULONG DefaultPagedPoolCharge; ULONG DefaultNonPagedPoolCharge; VOID (*DumpProcedure)( VOID* Object, struct _OBJECT_DUMP_CONTROL* DumpControl ); LONG (*OpenProcedure)( enum _OB_OPEN_REASON OpenReason, CHAR PreviousMode, struct _EPROCESS* Process, VOID* Object, ULONG* GrantedAccess, ULONG HandleCount ); VOID (*CloseProcedure)( struct _EPROCESS* Process, VOID* Object, ULONG GrantedAccess, ULONG HandleCount ); VOID (*DeleteProcedure)( VOID* Object ); LONG (*ParseProcedure)( VOID* ParsedObject, VOID* RemainingPath, struct _ACCESS_STATE* AccessState, CHAR AccessMode, ULONG Attributes, struct _UNICODE_STRING* CompleteName, struct _UNICODE_STRING* RemainingName, VOID* Context, struct _SECURITY_QUALITY_OF_SERVICE* SecurityQos, VOID** NewObject ); LONG (*SecurityProcedure)( VOID* Object, enum _SECURITY_OPERATION_CODE OperationCode, ULONG* SecurityInformation, VOID* SecurityDescriptor, ULONG* BufferLength, VOID** NewSecurityDescriptor, enum _POOL_TYPE PoolType, struct _GENERIC_MAPPING* Mapping, CHAR AccessMode ); LONG (*QueryNameProcedure)( VOID* Object, UCHAR HasAccess, struct _OBJECT_NAME_INFORMATION* NameInfo, ULONG NameInfoLength, ULONG* ReturnLength, CHAR AccessMode ); UCHAR (*OkayToCloseProcedure)( struct _EPROCESS* Process, VOID* Object, VOID* Handle, CHAR PreviousMode ); };
其中 SupportsObjectCallbacks
决定对象回调是否有效,如果将这个位置 0 则对象回调会失效。不过这个位被 PG 监控,不能长期修改。
另外 ValidAccessMask
表示的是可用的访问权限,如果这个位置 0 则该对象无法打开。例如内核中的 DbgkDebugObjectType
指向 DebugObject
的对象类型,如果把该类型的 ValidAccessMask
置 0 则会导致全局调试禁用。
OBJECT_TYPE_INITIALIZER
中还有一些列的回调函数,这些回调函数会针对不同内核对象的不同特性而指向不同的函数。
DumpProcedure
:供调试工具(如 WinDbg 的 !object
扩展)用于打印对象内部状态,包括类型、引用计数、属性等。
OpenProcedure
:当对象句柄打开时回调,允许拦截或修改访问请求。比如安全软件会在此处检查权限或拒绝打开。这个函数会在 ObpCreateHandle
内部处理时调用那个预操作(PreOperation)回调,以便对访问进行管理。
CloseProcedure
:句柄关闭时执行,常用于资源释放、日志记录或实现引用计数清理机制。在用户调用 NtClose
、CloseHandle
或句柄被回收时触发。
DeleteProcedure
:当对象未再被引用且要销毁前调用,允许释放分配的结构体、内存或执行其他最终清理工作。通常在 ObDereferenceObject
导致引用计数为零,内核准备释放该对象时调用。
ParseProcedure
:负责解析特定命名空间路径(如文件系统、设备路径等),并创建或引用对应对象。如从 \Device\...
路径进行 NtOpenFile
时,内核遍历命名空间,并在每个组件调用该回调函数。
SecurityProcedure
:例如在 NtQuerySecurityObject
、NtSetSecurityObject
等操作中,被调用以读取、安全验证、设置或重新编码安全描述符。通常在对象需要读取、设置或枚举安全信息时,在 ObpCreateHandle
或 ObQuerySecurityObject
等路径中触发。
QueryNameProcedure
:当用户通过 API 查询对象名称(如文件名、进程名、设备路径)时,负责填充输出缓冲区。在 NtQueryObject(..., ObjectNameInformation)
时调用,用于提供准确名称信息。
OkayToCloseProcedure
:在某些安全敏感或关键对象上使用,可拒绝某进程关闭句柄的请求,从而防止被篡改或持久化控制。在 NtClose
处理阶段,如果此回调返回 FALSE
,系统将拒绝关闭句柄。
例如我们在调用 NtClose(hObject)
有如下过程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 NtClose(hObject) │ ├─ ObpCloseHandle │ │ │ ├─ 🎯 调用 ObpCloseHandleTableEntry │ │ │ │ │ ├─ 🔍 调用 OkayToCloseProcedure(可拒绝句柄关闭) │ │ │ ├─ 返回 TRUE → 继续 │ │ │ └─ 返回 FALSE → ❌ STATUS_HANDLE_NOT_CLOSABLE │ │ │ │ │ └─ 🔢 调用 ObpDecrementHandleCount(递减句柄计数) │ │ │ │ │ └─ 🔔 调用 CloseProcedure(不可拒绝,仅通知行为) │ │ └─ 适用于日志、资源同步、Handle DB 清理等 │ │ │ └─ ObDereferenceObject │ │ │ └─ ObfDereferenceObjectWithTag │ │ │ └─ ObpRemoveObjectRoutine(对象引用为 0 时触发) │ │ │ ├─ 🔐 调用 SecurityProcedure(若注册) │ │ └─ 可校验、更新、释放安全描述符 │ │ │ └─ 💣 调用 DeleteProcedure(若注册) │ └─ 最终释放资源、销毁对象
OB_CALLBACK_HANDLE 当我们调用 ObRegisterCallbacks
注册回调函数成功后会返回一个 RegistrationHandle
句柄。实际上 RegistrationHandle
指向的是 OB_CALLBACK_HANDLE
(未公开)结构体。
这个结构体中保存了我们注册内核对象回调时的 OB_CALLBACK_REGISTRATION
结构体传递的信息,并且结构与 OB_CALLBACK_REGISTRATION
也非常相似。
1 2 3 4 5 6 7 8 9 10 11 12 typedef struct _OB_CALLBACK_HANDLE { USHORT Version; USHORT OperationRegistrationCount; PVOID RegistrationContext; UNICODE_STRING Altitude; OB_CALLBACK_ENTRY CallbackEntryArray[1 ]; } OB_CALLBACK_HANDLE, *POB_CALLBACK_HANDLE;
ObRegisterCallbacks
在申请 OB_CALLBACK_HANDLE
结构体所需内存时,会在 OB_CALLBACK_HANDLE
的最后额外多申请一部分内存,用于存放 Altitude
中的字符串。
其中 CallbackEntryArray
是一个 OB_CALLBACK_ENTRY
数组,对应着 OB_CALLBACK_REGISTRATION
的 OperationRegistration
字段。里面保存在我们注册的回调函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 typedef struct _OB_CALLBACK_ENTRY { LIST_ENTRY CallbackList; ULONG Operations; ULONG Flags; POB_CALLBACK_HANDLE CallbackHandle; POBJECT_TYPE ObjectType; POB_PRE_OPERATION_CALLBACK PreOperation; POB_POST_OPERATION_CALLBACK PostOperation; EX_RUNDOWN_REF RundownRef; } OB_CALLBACK_ENTRY, *POB_CALLBACK_ENTRY;
这个结构通过 CallbackList
字段挂到 OBJECT_TYPE
的 CallbackList
链表上。
对象回调分析 对象回调注册 ObRegisterCallbacks
根据我们传入的 CallbackRegistration
结构构造填充一个 POB_CALLBACK_HANDLE
结构,并调用 ObpInsertCallbackByAltitude
函数将其中 CallbackEntryArray
数组中的所有的 POB_CALLBACK_ENTRY
按照 Altitude
顺序挂到 OBJECT_TYPE
的 CallbackList
链表上。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 NTSTATUS NTAPI ObRegisterCallbacks ( POB_CALLBACK_REGISTRATION CallbackRegistration, PVOID* RegistrationHandle ) { USHORT RegistrationCount; SIZE_T AllocationSize; POB_CALLBACK_HANDLE CallbackHandle; POB_CALLBACK_ENTRY CallbackEntry; UNICODE_STRING* Altitude; WCHAR* AltitudeBuffer; NTSTATUS Status = STATUS_SUCCESS; ULONG i; if ((CallbackRegistration->Version & 0xFF00 ) != OB_FLT_REGISTRATION_VERSION) return STATUS_INVALID_PARAMETER; RegistrationCount = CallbackRegistration->OperationRegistrationCount; if (RegistrationCount == 0 ) return STATUS_INVALID_PARAMETER; AllocationSize = FIELD_OFFSET(OB_CALLBACK_HANDLE, CallbackEntryArray) + RegistrationCount * sizeof (OB_CALLBACK_ENTRY) + CallbackRegistration->Altitude.Length; CallbackHandle = (POB_CALLBACK_HANDLE)ExAllocatePoolWithTag(PagedPool, AllocationSize, 'ObCl' ); if (!CallbackHandle) return STATUS_INSUFFICIENT_RESOURCES; RtlZeroMemory(CallbackHandle, AllocationSize); CallbackHandle->Version = OB_FLT_REGISTRATION_VERSION; CallbackHandle->RegistrationContext = CallbackRegistration->RegistrationContext; CallbackHandle->OperationRegistrationCount = 0 ; AltitudeBuffer = (WCHAR*)((PUCHAR)CallbackHandle + AllocationSize - CallbackRegistration->Altitude.Length); RtlCopyMemory(AltitudeBuffer, CallbackRegistration->Altitude.Buffer, CallbackRegistration->Altitude.Length); Altitude = &CallbackHandle->Altitude; Altitude->Length = CallbackRegistration->Altitude.Length; Altitude->MaximumLength = Altitude->Length; Altitude->Buffer = AltitudeBuffer; for (i = 0 ; i < RegistrationCount; ++i) { const OB_OPERATION_REGISTRATION* OperationRegistration = &CallbackRegistration->OperationRegistration[i]; POBJECT_TYPE ObjectType = *OperationRegistration->ObjectType; if (!OperationRegistration->Operations || !ObjectType->TypeInfo.SupportsObjectCallbacks) { Status = STATUS_INVALID_PARAMETER; break ; } if (OperationRegistration->PreOperation && !MmVerifyCallbackFunction((PVOID)OperationRegistration->PreOperation)) { Status = STATUS_ACCESS_DENIED; break ; } if (OperationRegistration->PostOperation && !MmVerifyCallbackFunction((PVOID)OperationRegistration->PostOperation)) { Status = STATUS_ACCESS_DENIED; break ; } CallbackEntry = &CallbackHandle->CallbackEntryArray[i]; CallbackEntry->Operations = OperationRegistration->Operations; CallbackEntry->CallbackHandle = CallbackHandle; CallbackEntry->ObjectType = ObjectType; CallbackEntry->PreOperation = OperationRegistration->PreOperation; CallbackEntry->PostOperation = OperationRegistration->PostOperation; CallbackEntry->RundownRef = NULL ; InitializeListHead(&CallbackEntry->CallbackList); Status = ObpInsertCallbackByAltitude(CallbackEntry, ObjectType); if (!NT_SUCCESS(Status)) break ; CallbackHandle->OperationRegistrationCount++; } if (!NT_SUCCESS(Status)) { for (i = 0 ; i < CallbackHandle->OperationRegistrationCount; ++i) { POB_CALLBACK_ENTRY Entry = &CallbackHandle->CallbackEntryArray[i]; POBJECT_TYPE ObjectType = Entry->ObjectType; EX_PUSH_LOCK* Lock = &ObjectType->TypeLock; KeEnterCriticalRegion(); ExAcquirePushLockExclusive(Lock); RemoveEntryList(&Entry->CallbackList); ExReleasePushLockExclusive(Lock); KeLeaveCriticalRegion(); } ExFreePoolWithTag(CallbackHandle, 'ObCl' ); } else { for (i = 0 ; i < CallbackHandle->OperationRegistrationCount; ++i) { CallbackHandle->CallbackEntryArray[i].Flags |= OB_CALLBACK_ACTIVE_FLAG; } *RegistrationHandle = CallbackHandle; } return Status; }
ObpInsertCallbackByAltitude
函数则会根据 Altitude
找到合适的位置插入链表。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 NTSTATUS ObpInsertCallbackByAltitude ( POB_CALLBACK_ENTRY CallbackEntry, POBJECT_TYPE ObjectType ) { PLIST_ENTRY CallbackListHead; PLIST_ENTRY Current; UNICODE_STRING* Altitude; NTSTATUS Status = STATUS_SUCCESS; KeEnterCriticalRegion(); ExAcquirePushLockExclusive(&ObjectType->TypeLock); CallbackListHead = &ObjectType->CallbackList; Current = CallbackListHead->Flink; Altitude = &CallbackEntry->CallbackHandle->Altitude; while (Current != CallbackListHead) { POB_CALLBACK_ENTRY Entry = CONTAINING_RECORD(Current, OB_CALLBACK_ENTRY, CallbackList); POB_CALLBACK_HANDLE Registration = Entry->CallbackHandle; int CompareResult = RtlCompareAltitudes( &Registration->Altitude, Altitude ); if (CompareResult <= 0 ) { if (CompareResult == 0 ) { Status = STATUS_FLT_INSTANCE_ALTITUDE_COLLISION; } break ; } Current = Current->Flink; } if (NT_SUCCESS(Status)) { InsertTailList(Current->Blink, &CallbackEntry->CallbackList); } ExReleasePushLockExclusive(&ObjectType->TypeLock); KeLeaveCriticalRegion(); return Status; }