Windows 回调机制
😄
1 CALLBACK_OBJECT
本文是内核层的 WDK 开发,并不是 3 环的开发。
本文分析系统版本:

CALLBACK_OBJECT 是一种内核命名对象(匿名的回调对象是无意义的),内核中使用基于改内核回调对象的回调机制。注意这里的回调机制区别于 NotifyRoutine 回调函数。
回调机制为驱动程序提供了一种通用的方法来发送和接收某类通知,这些通告可以是系统某个部件的状态发生变化而产生的通告,也 可以是开发者自定义的某个条件因为条件成立而产生的通告。
系统已经定义三个回调对象供驱动程序使用,每个回调对象都有一个名称和一组属性,这些属性是在创建对象时定义的:
- \Callback\SetSystemTime,当系统时间发生变化时,此回调提供通知。
- \Callback\PowerState,当发生以下情况之一时,
\Callback\PowerState回调对象将提供通知:- 系统从交流电源切换到直流电电源,反之亦然。
- 系统电源策略因用户或应用程序请求而更改。
- 即将过渡到系统睡眠或关闭状态。 驱动程序可以请求通知,以便可以将代码锁定到内存中,以便于关闭。 在电源管理器发送系统设置-power IRP 之前,会通知回调例程。
- \Callback\ProcessorAdd,将新处理器添加到系统时,
\Callback\ProcessorAdd回调会提供通知。
回调对象的使用分为以下几个步骤:
- 创建回调对象(ExCreateCallback),创建回调对象之前需要先调用 InitializeObjectAttributes 来设置其属性。
- 注册回调例程(ExRegisterCallback),将回调函数注册到回调对象上。
- 触发回调通知(ExNotifyCallback),指定条件发生时可以调用该函数触发回调函数执行。
- 卸载已经注册的回调例程(ExUnregisterCallback )。
- 回调对象解引用(ObDereferenceObject),在删除回调对象之前调用。
- 删除回调对象。
_CALLBACK_OBJECT 回调对象结构如下:
1 | typedef struct _CALLBACK_OBJECT |
1.1 创建回调对象

在创建对象之前,驱动程序调用 InitializeObjectAttributes 宏来设置其属性。注意:IRQL <= APC_LEVEL。
1 | VOID InitializeObjectAttributes( |
当回调对象的属性初始化好之后,便可以调用 ExCreateCallback 函数进行对象创建。回调对象必须具有一个名称,该名称不能与系统定义的回调的名称想同。不允许使用未命名的回调对象,调用方必须指定对象的名称和属性。
ExCreateCallback 例程可以创建新的回调对象或代表调用方打开现有回调对象。
1 | NTSTATUS ExCreateCallback( |
返回值:如果打开或创建回调对象,将返回 STATUS_SUCCESS。 否则,它将返回 NTSTATUS 错误代码来指示失败的性质。
1.2 注册回调例程
回调对象创建好后,需要调用 ExRegisterCallback 函数来将回调对象与回调例程进行绑定。注意:IRQL <= APC_LEVEL。
1 | PVOID ExRegisterCallback( |
返回值:如果注册成功则返回 PVOID 类型的注册句柄,该句柄应被视为不透明并保留供系统使用(如给 ExUnregisterCallback 使用)。如果注册失败则返回 NULL。
回调例程定义如下:
1 | VOID (*PCALLBACK_FUNCTION ) ( |
如果可以在 DISPATCH_LEVEL 调用回调例程,调用方应从非分页池分配 CallbackContext。
1.3 触发通知
注意:IRQL <= DISPATCH_LEVEL。
ExNotifyCallback 例程会导致指定回调对象上的所有回调例程得到执行。
1 | void ExNotifyCallback( |
驱动程序必须在 IRQL <= DISPATCH_LEVEL 调用 ExNotifyCallback;系统必须在驱动程序调用此调用的同一 IRQL 处调用回调例程。
注意:调用 ExNotifyCallback 会使回调对象上的所有回调例程按顺序都得到执行,并不能指定去执行某一个回调例程。
使用示例:
- RactOS-callback.c
- Github-PowerCallback
- Github-WindowsNotifyAndCallback.c
- Windows ExCreateCallback多驱动间通信
- Windows Kernel Ps Callbacks Experiments
1.4 逆向分析ExCreateCallback
调用 ExCreateCallback 之前需要先调用 InitializeObjectAttributes 宏对回调对象 CALLBACK_OBJECT 属性进行初始化。
1 | PAGE:00000001407105D0 // =============== S U B R O U T I N E ======================================= |
2 注册表回调
本文先不研究注册表对象的创建、初始化等(CmpCreateObjectTypes/CmpInitCallbacks/CmpDeleteKeyObject)。
注册表操作的通知实际意义上不是上一篇文章进程、线程、模块通知类型,因为没有使用到 EX_CALLBACK、EX_CALLBACK_ROUTINE_BLOCK 结构来描述通知例程。所以我将其归为回调类操作。
如下图所示,对注册表回调操作的流程。

2.1 创建/删除回调例程
- 创建注册表回调例程:CmRegisterCallback、CmRegisterCallbackEx。
- 删除注册表回调例程:CmUnRegisterCallback。
使用 CmRegisterCallback 安装注册表回调函数,从 Vista 开始将使用 CmRegisterCallbackEx 函数(不推荐使用前者),因为参数内容更丰富。本文主要分析 CmRegisterCallbackEx。可以看到:这两个函数所创建的回调例程都是 PEX_CALLBACK_FUNCTION 类型。
1 | NTSTATUS CmRegisterCallback( |
返回值:
创建成功:STATUS_SUCCESS。
创建失败:返回的 STATUS 是以下之一。
返回代码 说明 STATUS_FLT_INSTANCE_ALTITUDE_COLLISION 调用驱动程序或其他驱动程序已经为指定的高度Altitude注册了 RegistryCallback 例程。 STATUS_INSUFFICIENT_RESOURCES 尝试分配内存失败。
删除回调例程的函数是 CmUnRegisterCallback:
1 | NTSTATUS CmUnRegisterCallback( |
返回值:
- 成功:STATUS_SUCCESS。
- 如果
Cookie与已安装的注册表回调例程不匹配,则返回STATUS_INVALID_PARAMETER。 - 其他 NTSTATUS 错误代码。
2.2 注册表回调例程
注册表回调例程可以监视、阻止或修改注册表操作,原型如下:
1 | NTSTATUS ExCallbackFunction( |
Argument2 指向一个结构,该结构的类型由 Argument1 决定,是有对应关系的,如下图(具体见 ExCallbackFunction)。

其中的几个比较常用的类型,它们的意义,以及对应的 Argument2 的结构体的内容如下,都是 REG_XXX_KEY_INFORMATION 数据结构:
| REG_NOTIFY_CLASS的值 | 意义 | Argument2结构体 |
|---|---|---|
| RegNtPreCreateKey | 创建注册表之前 | PREG_CREATE_KEY_INFORMATION |
| RegNtPreOpenKey | 打开注册表之前 | PREG_CREATE_KEY_INFORMATION |
| RegNtPreDeleteKey | 删除键之前 | PREG_DELETE_KEY_INFORMATION |
| RegNtPreDeleteValueKey | 删除键值之前 | PREG_DELETE_VALUE_KEY_INFORMATION |
| RegNtPreSetValueKey | 修改键值之前 | PREG_SET_VALUE_KEY_INFORMATION |
返回值:
- Vista之前:该函数的返回值为
STATUS_SUCCESS之外的错误码的时候,则表示系统会拒绝操作相应的注册表。 - Vista及之后:增加了
STATUS_CALLBACK_BYPASS返回值,可以使配置管理器停止处理这个注册表请求,并且返回STATUS_SUCCESS给调用线程。
注意,对注册表的每种操作都有操作前(Pre)调用回调例程,和操作后(Post)调用回调例程。如
RegNtPreOpenKey表示打开指定注册表项前调用回调例程。RegNtPostOpenKey表示打开指定注册表项后调用回调例程。
我们一般使用操作前的 REG_NOTIFY_CLASS,方便做拦截。在回调例程中直接返回 STATUS_ACCESS_DENIED 即可。
2.3 函数逆向分析
CmRegisterCallback
1 | PAGE:00000001407C64E0 ; NTSTATUS __stdcall CmRegisterCallback(PEX_CALLBACK_FUNCTION Function, PVOID Context, PLARGE_INTEGER Cookie) |
从上面的代码分析来看,函数还原如下:
1 | NTSTATUS __stdcall CmRegisterCallback(PEX_CALLBACK_FUNCTION Function, PVOID Context, PLARGE_INTEGER Cookie) |
注册表回调结构:
1 | PREGISTRY_CALLBACK Callback; |
CmRegisterCallbackEx
1 | /* |
函数还原如下:
1 | NTSTATUS CmRegisterCallbackEx( |
CmpRegisterCallbackInternal
1 | PAGE:00000001406F3B98 // =============== S U B R O U T I N E ======================================= |
函数还原得:
1 | NTSTATUS CmpRegisterCallbackInternal |
REGISTRY_CALLBACK 结构如下:
1 | PREGISTRY_CALLBACK Callback; |
POOL_TYPE 的枚举值如下:
1 | //0x4 bytes (sizeof) |
CmpInsertCallbackInListByAltitude
1 | // CmpInsertCallbackInListByAltitude(pCmpCallback, *(PUCHAR)Altitude->Length/*0/1*/); |
函数还原得:
1 | NTSTATUS CmpInsertCallbackInListByAltitude(pCmpCallback, Altitude/*0/1*/) |
CmUnRegisterCallback
1 | NTSTATUS __stdcall CmUnRegisterCallback(LARGE_INTEGER Cookie) |
CmpCallCallBacks
1 | __int64 __fastcall CmpCallCallBacks(int a1, __int64 a2, __int64 a3, __int64 a4, __int64 a5, __int64 a6) |
CmpCallCallBacksEx
1 | __int64 __fastcall CmpCallCallBacksEx(unsigned int a1, __int64 a2, __int64 a3, char a4, unsigned int a5, __int64 a6, __int64 a7) |
CmpEnumerateCallback
1 | PAGE:000000014076E620 CmpEnumerateCallback proc near // DATA XREF: .rdata:000000014009B5E0↑o |
参考:
- 通过CmRegisterCallback学习注册表监控与反注册表监控
- 深入注册表监控
- 驱动开发:内核监控Register注册表回调
- ring0-PsSetLoadImageNotifyRoutine
- Kernel Karnage – Part 5 (I/O & Callbacks)
- Deep Analysis of a Driver-Based MITM Malware: iTranslator
- Win64 驱动内核编程-15.回调监控注册表
2.4 总结
所有的注册表回调例程都放在
CallbackListHead链表表尾,该链表每一个成员都是REGISTRY_CALLBACK结构。CallbackListHead挂着所有注册表回调函数所在的REGISTRY_CALLBACK结构:![15.png]()
回调例程是存放在
REGISTRY_CALLBACK结构中的。1
2
3
4
5
6
7
8
9
10
11typedef struct _REGISTRY_CALLBACK
{
0x0 LIST_ENTRY ListEntry;
0x10 LARGE_INTEGER Cookie; // EX_RUNDOWN_REF RundownRef;
0x18 PEX_CALLBACK_FUNCTION Function;
0x20 PVOID Context;
0x28 PEX_CALLBACK_FUNCTION Function; //LARGE_INTEGER Cookie;
0x30 BOOLEAN PendingDelete;
0x38 ULONGLONG AllocateAltitudeReturnValue;
0x40 LIST_ENTRY ListEntry2;
} REGISTRY_CALLBACK, *PREGISTRY_CALLBACK;全局变量
CmpCallbackCookie在每一次插入一个新的回调例程时都是自增1。CmpCallBackCount用来表示CallbackListHead链表上回调例程个数的一个全局变量。每次对链表
CallbackListHead进行操作前,都要使用全局变量CmpCallbackListLock来对链表进行互斥上锁操作。

3 对象回调
在对内核对象句柄之前、之后的操作,都可以使用对象回调例程来进行监控并阻止。

3.1 创建/删除对象回调例程
创建对象句柄回调例程的函数是 ObRegisterCallbacks ,删除对象句柄回调例程的函数是 ObUnRegisterCallbacks。
1 | NTSTATUS ObRegisterCallbacks( |
参数一:
OB_CALLBACK_REGISTRATION原型如下:1
2
3
4
5
6
7typedef struct _OB_CALLBACK_REGISTRATION {
USHORT Version; // 版本,驱动程序应指定 OB_FLT_REGISTRATION_VERSION == ObGetFilterVersion()
USHORT OperationRegistrationCount; // 参数五 OperationRegistration 数组中的元素个数
UNICODE_STRING Altitude; // 回调函数被调用优先级(顺序),可以任意设置一个整数
PVOID RegistrationContext; // 传递给回调例程的参数
OB_OPERATION_REGISTRATION *OperationRegistration; // 指针,指向 OB_OPERATION_REGISTRATION 结构的数组
} OB_CALLBACK_REGISTRATION, *POB_CALLBACK_REGISTRATION;OperationRegistration成员是一个数组,该数组的个数由OperationRegistrationCount指定。每个元素都是OB_OPERATION_REGISTRATION结构,原型如下:1
2
3
4
5
6typedef struct _OB_OPERATION_REGISTRATION {
POBJECT_TYPE *ObjectType;
OB_OPERATION Operations;
POB_PRE_OPERATION_CALLBACK PreOperation;
POB_POST_OPERATION_CALLBACK PostOperation;
} OB_OPERATION_REGISTRATION, *POB_OPERATION_REGISTRATION;成员 含义 ObjectType 指向触发回调例程的对象类型的指针。 指定以下值之一:
PsProcessType:用于进程句柄操作
PsThreadType:用于线程句柄操作
ExDesktopObjectType:用于桌面句柄操作。此值在 Windows 10 之前的系统中不支持Operations OB_OPERATION结构,表示对句柄做什么操作。指定以下一个或多个标志:OB_OPERATION_HANDLE_CREATE:已打开或将要打开一个新的进程,线程或桌面句柄OB_OPERATION_HANDLE_DUPLICATE:已复制或将要复制进程,线程或桌面句柄PreOperation 指向回调例程。在请求操作发生之前,系统调用此例程。 POB_PRE_OPERATION_CALLBACK。PostOperation 指向回调例程。在请求操作发生之后,系统调用此例程。 POB_POST_OPERATION_CALLBACK。POB_PRE_OPERATION_CALLBACK、POB_POST_OPERATION_CALLBACK回调例程原型都是一样的:1
2
3
4
5
6
7POB_PRE_OPERATION_CALLBACK PobPreOperationCallback;
OB_PREOP_CALLBACK_STATUS PobPreOperationCallback(
[in] PVOID RegistrationContext, // 回调函数的参数,由 ObRegisterCallbacks 函数指定
[in] POB_PRE_OPERATION_INFORMATION OperationInformation //重要的参数
)
{...}OperationInformation:可以通过修改这个参数,拒绝某些操作的权限,以达到保护进程、线程的目的。它是
OB_PRE_OPERATION_INFORMATION结构:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15typedef struct _OB_PRE_OPERATION_INFORMATION {
OB_OPERATION Operation; // 句柄操作的类型,此时从Parameters.CreateHandleInformation获取详细信息
union {
ULONG Flags; // 保留,请改用 KernelHandle 成员
struct {
ULONG KernelHandle : 1; // 指定句柄是否为内核句柄, TRUE:内核句柄
ULONG Reserved : 31; // 预留给系统使用
};
};
PVOID Object; // 当前句柄所属的进程/线程对象
POBJECT_TYPE ObjectType; // 对象类型,只能是 PsThreadType 或 PsProcessType
PVOID CallContext; // 同回调例程参数一 RegistrationContext
POB_PRE_OPERATION_PARAMETERS Parameters; // 指向包含特定于操作信息的 OB_PRE_OPERATION_PARAMETERS 联合的指针。
// Operation 成员确定联合的哪个成员有效。
} OB_PRE_OPERATION_INFORMATION, *POB_PRE_OPERATION_INFORMATION;Parameters是一个OB_PRE_OPERATION_PARAMETERS联合结构:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16typedef 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;
typedef struct _OB_PRE_CREATE_HANDLE_INFORMATION {
ACCESS_MASK DesiredAccess; // 指定要授予句柄的访问权限,在回调例程可以修改权限
ACCESS_MASK OriginalDesiredAccess; // 句柄原始的访问权限
} OB_PRE_CREATE_HANDLE_INFORMATION, *POB_PRE_CREATE_HANDLE_INFORMATION;
typedef struct _OB_PRE_DUPLICATE_HANDLE_INFORMATION {
ACCESS_MASK DesiredAccess; // 指定要授予句柄的访问权限,在回调例程可以修改权限
ACCESS_MASK OriginalDesiredAccess; // 句柄原始的访问权限
PVOID SourceProcess; // A pointer to the process object for the process that is the source of the handle.
PVOID TargetProcess; // A pointer to the process object for the process that receives the duplicated handle.
} OB_PRE_DUPLICATE_HANDLE_INFORMATION, *POB_PRE_DUPLICATE_HANDLE_INFORMATION;DesiredAccess 默认情况下和
OriginalDesiredAccess权限是一样的,即拥有全部权限。在回调函数中可修改该值,可修改的权限类型分为两类:- 进程权限,如
PROCESS_TERMINATE(可让进程终止的权限) - 线程权限,如
THREAD_SET_CONTEXT(可修改线程上下文的权限)
- 进程权限,如
参数二:
RegistrationHandle。接受注册成功后的handle,在驱动卸载或反注册时需要调用ObUnRegisterCallbacks,并把该参数传入。
可以看到操作前操作后的 PreOperation、PostOperation 函数对应的 OperationInformation 不同:
- 操作前,可以对
OB_PRE_OPERATION_INFORMATION.Parameters.CreateHandleInformation.DesiredAccess权限进行操作。 - 操作后,可以对
OB_POST_OPERATION_INFORMATION.ReturnStatus返回码进行操作。
句柄操作前 POB_PRE_OPERATION_CALLBACK 参数结构及句柄操作后 POB_POST_OPERATION_CALLBACK 参数结构如下:

3.2 ObRegisterCallbacks 分析
1 | NTSTATUS ObRegisterCallbacks( |
上述结构分析还是有一些问题,具体参考 《ObRegisterCallBacks的初始化与插入》。
1 | 返回的结构类型为:_CALLBACK_ENTRY |
ObRegisterCallbacks:
1 | PAGE:00000001407BB720 // NTSTATUS ObRegisterCallbacks( |
ObUnRegisterCallbacks
ObpCallPreOperationCallbacks
ObpCallPostOperationCallbacks
