第9章 对象和注册表通知
用对象操作前回调拦截句柄权限实现进程保护,用操作后回调登记审计,再实现注册表前后通知与卸载注销的完整纪律。
学习目标
- 能解释 ObRegisterCallbacks 的注册结构(对象类型、操作类别、海拔)与前/后回调的分工
- 能改出“受保护进程列表 + 前回调拦截权限 + 后回调登记审计”的进程保护驱动,并在卸载时正确注销
- 能回答:为什么拦截必须发生在操作前回调里,操作后回调只能观察结果?
为什么大楼门口要装安检,档案室要装登记岗
想象一栋大楼:访客要进楼(程序想打开某个进程),保安在门口查证件——证件不合规,当场拦下;人进去之后,监控只负责记录“谁进了哪间房”,不可能再把进去的人拽出来。“拦”和“记”是两件必须分开的事。大楼里还有一间档案室(注册表),谁的档案被翻看、被改动,都要过登记岗:改动前登记岗可以拒绝,改动后只能留一笔记录。
没有这套系统会怎样?任何程序都能随意打开受保护进程,安全软件只能干瞪眼;注册表被偷偷改掉,没人知道、也没人拦得住。
这一章装这两套系统:大楼门口的安检(对象通知)和档案室的登记岗(注册表通知),最后学会安全撤岗(卸载注销)——撤岗的纪律和装岗一样重要。
对象通知:给句柄操作装一道安检门
↡内核在句柄被打开、复制或关闭时调用驱动注册的回调,通过 ObRegisterCallbacks 注册 让你在“获得对象句柄”这条必经之路上设卡。注册时要声明三件事:监控哪类对象(进程 PsProcessType、线程 PsThreadType)、哪些操作(OB_OPERATION_HANDLE_CREATE 创建、OB_OPERATION_HANDLE_DUPLICATE 复制)、以及海拔(Altitude,一串字符串,决定你的回调排在别的回调之前还是之后)。
注册成功会返回一个注册句柄(Cookie)——这是你的“上岗证”,卸载时必须用它注销。它的位置在句柄操作路径上:任何进程想打开受保护进程的句柄,都要先过这道门。
操作前回调:在门口把权限拦下来
↡句柄操作真正发生之前调用的回调,可以检查并修改请求的访问权限,或直接拒绝操作 是进程保护的拦截点:查受保护列表 → 命中 → 清除危险权限。注意是“清除权限”而不是“直接拒绝”——系统里有些组件确实需要拿到受限句柄,一律拒绝会把系统搞崩。
回调返回后句柄按修改过的权限继续创建,所以真正落地的权限是你改过的版本。内核自己打开的句柄(KernelHandle 字段)必须放行:拦截内核会让系统无法启动。这个回调里只能查自旋锁保护的数据、做位运算,不能等待。
操作后回调:记一笔审计
↡句柄操作完成之后调用的回调,只能观察操作结果,不能改变任何东西 和门口安检正好互补:人已经进去了,监控补记一笔——谁(当前进程 ID)、什么时间、对哪个对象、要了什么权限、是创建还是复制。
它不能拒绝任何操作(操作已经发生),也不该做重活。前回调拦不拦是策略,后回调记不记是证据。安全产品的“谁碰过我的进程”审计,就是从这里来的。
注册表通知:档案室的登记岗
↡内核在每次注册表读写、删键、改名等操作时调用的回调,通过 CmRegisterCallbackEx 注册 覆盖全部注册表操作。回调的第一个参数是操作分类(REG_NOTIFY_CLASS):RegNtSetValueKey(写值之前)、RegNtPostSetValueKey(写值之后)、RegNtDeleteKey(删键之前)……前通知可以拒绝(返回 STATUS_ACCESS_DENIED),后通知只能读到操作结果。
回调运行在 PASSIVE_LEVEL,但注册表操作极高频——回调必须短小,只做“查键路径 + 比较 + 返回”,不能打开文件、不能等锁。注册表过滤是安全产品的基础设施,做慢了整个系统变卡。
用户模式客户:策略从哪来
↡通过 IOCTL 向驱动下发保护策略、读取审计记录的用户程序 回答“保护谁”的问题:受保护进程列表不该硬编码在驱动里,由客户程序通过 IOCTL 下发,驱动维护列表,前回调查表拦截;审计记录同样由客户程序取走。
配置通道低频小包(改一次列表),通知通道高频批量(审计记录),与第 8 章的通知管道同构。驱动侧永远只认列表不认人——策略是数据,不是代码。
动手:走一遍对象与注册表通知
下面的可视化把两套系统画成一张图:上半部分是对象通知——选“打开句柄/复制句柄”,看前回调怎么检查、后回调怎么审计;打开“进程保护”开关后再点“打开句柄”,观察拦截效果。下半部分是注册表通知——选查询/设置/删除,看前通知与后通知的分流。
猜一猜:打开“进程保护”开关后,再点击“打开句柄”——前回调框会变成什么颜色?操作结果条会显示什么?后回调框为什么显示“未执行”?
操作前回调在句柄操作真正发生之前调用,可以:检查访问掩码(AccessMask)、拒绝操作(返回 STATUS_ACCESS_DENIED)、给操作附加上下文。这是实现进程保护的关键位置——把目标进程的句柄操作拦在门口。
代码对照:进程保护 + 注册表通知的最小实现
这一节按原书结构拆解五段核心代码:注册对象回调、前回调拦截、后回调审计、注册表回调、卸载注销。五个段覆盖驱动侧全部关键代码,完整工程见章末来源链接。
段一:注册对象回调 —— 上岗证怎么领
入口里先填一张“登记表”(OB_OPERATION_REGISTRATION 数组):每个元素声明一类对象、一组操作和对应的前/后回调;再填总登记表(OB_CALLBACK_REGISTRATION),声明海拔和注册上下文。最后 ObRegisterCallbacks 领到上岗证 g_regHandle。
// 注册对象回调:监控进程句柄的创建与复制
NTSTATUS RegisterObjectCallbacks()
{
OB_OPERATION_REGISTRATION ops[2] = { 0 };
OB_CALLBACK_REGISTRATION reg = { 0 };
reg.Version = OB_FLT_REGISTRATION_VERSION;
reg.OperationRegistrationCount = 2;
reg.Altitude = L"320000"; // 海拔:决定回调调用顺序
reg.RegistrationContext = nullptr;
reg.OperationRegistration = ops;
ops[0].ObjectType = PsProcessType; // 监控进程对象
ops[0].Operations = OB_OPERATION_HANDLE_CREATE |
OB_OPERATION_HANDLE_DUPLICATE;
ops[0].PreOperation = OnPreOpenProcess;
ops[0].PostOperation = OnPostOpenProcess;
ops[1].ObjectType = PsThreadType; // 监控线程对象
ops[1].Operations = OB_OPERATION_HANDLE_CREATE;
ops[1].PreOperation = OnPreOpenThread;
ops[1].PostOperation = OnPostOpenThread;
// 注册成功后 g_regHandle 是注销凭证
return ObRegisterCallbacks(®, &g_regHandle);
}RegisterObjectCallbacks
│
├─→ 填 ops[]:每项 = 对象类型 + 操作类别 + 前/后回调
│ · PsProcessType + CREATE|DUPLICATE → OnPre/OnPostOpenProcess
│ · PsThreadType + CREATE → OnPre/OnPostOpenThread
├─→ 填 reg:版本 + 数量 + 海拔 + 上下文 + ops[]
├─→ ObRegisterCallbacks(®, &g_regHandle)
│ └─→ 失败:返回 NTSTATUS,无证上岗
└─→ 成功:g_regHandle = 上岗证(卸载时凭它注销)段二:前回调拦截 —— 进程保护的核心
回调体只有三步:放行内核句柄 → 查保护列表 → 命中则清掉危险权限。危险权限按需裁剪:PROCESS_TERMINATE(结束进程)、PROCESS_VM_WRITE(写内存)、PROCESS_SUSPEND_RESUME(挂起恢复)。清完权限返回成功,句柄以“瘦身版”权限继续创建——调用方拿到的句柄权限已被收窄。
// 操作前回调:受保护进程的危险权限在门口被清掉
OB_PREOP_CALLBACK_STATUS OnPreOpenProcess(
PVOID ctx, POB_PRE_OPERATION_INFORMATION Info)
{
UNREFERENCED_PARAMETER(ctx);
if (Info->KernelHandle) // 内核自己开的句柄:放行
return OB_PREOP_SUCCESS;
PEPROCESS proc = (PEPROCESS)Info->Object;
if (!IsProtected(proc)) // 不在保护列表:放行
return OB_PREOP_SUCCESS;
// 命中保护:清掉危险权限,保留只读权限
Info->Parameters.CreateHandleInformation.DesiredAccess &=
~(PROCESS_TERMINATE | PROCESS_VM_WRITE |
PROCESS_SUSPEND_RESUME | PROCESS_CREATE_PROCESS);
return OB_PREOP_SUCCESS;
}任何进程打开/复制进程句柄
│
▼
OnPreOpenProcess(句柄操作前)
│
├─ KernelHandle 非空? → 放行(内核不能被拦)
├─ 不在受保护列表? → 放行
└─ 命中保护:
DesiredAccess &= ~(TERMINATE|VM_WRITE|...)
→ 句柄以收窄后的权限创建段三:后回调审计 —— 只记录,不拦截
操作完成后内核再调一次后回调:把“谁、对哪个进程、什么操作、要了什么权限”写进审计缓冲。注意它拿到的权限是请求的权限(可能已被前回调收窄,也可能本来就是受限请求)——审计的职责是如实记录请求,而不是判断好坏。
// 操作后回调:只记录,不拦截
VOID OnPostOpenProcess(PVOID ctx,
POB_POST_OPERATION_INFORMATION Info)
{
UNREFERENCED_PARAMETER(ctx);
if (Info->KernelHandle)
return;
ULONG access = Info->Parameters.
CreateHandleInformation.DesiredAccess;
LogAudit(PsGetCurrentProcessId(), // 谁开的
(ULONG_PTR)Info->Object, // 对谁开的
Info->Operation, // 创建 or 复制
access); // 要了什么权限
}句柄操作已完成
│
▼
OnPostOpenProcess(操作后)
│
├─ 只能观察:读取请求的权限、操作类型
├─ 不能拒绝:操作已经发生
└─ 写审计记录:谁 / 对谁 / 什么操作 / 什么权限
(数据交给用户模式客户取走)段四:注册表回调 —— 一张回调,两种身份
CmRegisterCallbackEx 只需要一个回调函数,内核靠第一个参数(REG_NOTIFY_CLASS)告诉你是哪种操作、是前还是后。前通知返回 STATUS_ACCESS_DENIED 即可拒绝该操作;后通知只能读结果。参数对象(info->Object)是注册表键对象的句柄,可用 ZwQueryKey 之类查询路径——但回调里禁止等待,查询要在允许的范围内做。
// 注册注册表通知回调
NTSTATUS RegisterRegistryCallback(PDRIVER_OBJECT Drv)
{
ANSI_STRING altitude = RTL_CONSTANT_STRING("320000");
return CmRegisterCallbackEx(OnRegistryNotify,
&altitude, Drv, nullptr, &g_regCookie);
}
// 回调按操作分类分流:前通知可拒绝,后通知只观察
NTSTATUS OnRegistryNotify(PVOID ctx, PVOID Arg1, PVOID Arg2)
{
UNREFERENCED_PARAMETER(ctx);
REG_NOTIFY_CLASS op = (REG_NOTIFY_CLASS)(ULONG_PTR)Arg1;
if (op == RegNtSetValueKey) { // 写值之前
auto* info = (REG_SET_VALUE_KEY_INFORMATION*)Arg2;
if (IsSensitiveKey(info->Object))
return STATUS_ACCESS_DENIED; // 拒绝写敏感键
}
else if (op == RegNtDeleteKey) { // 删键之前
auto* info = (REG_DELETE_KEY_INFORMATION*)Arg2;
if (IsProtectedKey(info->Object))
return STATUS_ACCESS_DENIED; // 拒绝删保护键
}
else if (op == RegNtPostSetValueKey) {
// 写值之后:只能观察结果,不能拦截
}
return STATUS_SUCCESS;
}注册表操作发生
│
▼
OnRegistryNotify(PASSIVE_LEVEL,必须短小)
│
├─ Arg1 = 操作分类(REG_NOTIFY_CLASS)
│
├─ 前通知(RegNtSetValueKey / RegNtDeleteKey ...)
│ ├─ 敏感键? → 返回 STATUS_ACCESS_DENIED(拒绝)
│ └─ 普通键 → 返回 STATUS_SUCCESS(放行)
│
└─ 后通知(RegNtPost*)
└─ 只能读结果,不能拒绝段五:卸载注销 —— 撤岗的顺序不能乱
卸载是整章纪律的总验收:先注销对象回调(ObUnRegisterCallbacks 返回时保证没有正在执行的回调),再注销注册表回调(CmUnRegisterCallback 返回后不再有新通知),然后释放回调引用的全部数据,最后删设备。顺序颠倒——先释放列表再注销——回调可能正查着已释放的链表,直接蓝屏。
VOID OnUnload(PDRIVER_OBJECT DriverObject)
{
// 1. 注销对象回调:返回后内核不再调用
if (g_regHandle)
ObUnRegisterCallbacks(g_regHandle);
// 2. 注销注册表回调:返回后不再有新通知
if (g_regCookie)
CmUnRegisterCallback(g_regCookie);
// 3. 释放受保护列表与审计缓冲
FreeProtectList();
FreeAuditBuffer();
// 4. 删除设备对象
IoDeleteDevice(DriverObject->DeviceObject);
}OnUnload
│
├─ 1. ObUnRegisterCallbacks(g_regHandle) 撤岗
├─ 2. CmUnRegisterCallback(g_regCookie) 撤岗
│ (两个注销都返回 → 回调保证不再被调用)
├─ 3. 释放保护列表 / 审计缓冲 拆岗亭
└─ 4. IoDeleteDevice 拆大门
顺序不可颠倒:先拆岗亭再撤岗 = 蓝屏容易踩的坑
小结
- 对象通知经 ObRegisterCallbacks 注册:对象类型 + 操作类别 + 海拔,返回注册句柄
- 前回调是拦截点:查保护列表、收窄 DesiredAccess,内核句柄放行
- 后回调只做审计:谁/对谁/什么操作/什么权限,不能拒绝
- 注册表通知一张回调两种身份:前通知可拒绝,后通知只观察
- 卸载先注销两个回调,返回后再释放资源,顺序不可颠倒
练习
问题 1: 在 Demo 中打开“进程保护”开关后点击“打开句柄”,观察前回调框变红和结果条。为什么“清除危险权限”比“直接拒绝所有打开”更稳妥?如果改成直接拒绝,哪些场景会出问题?
问题 2: 你的注册表回调需要“禁止任何进程删除 HKLM\Software\MyApp 下的键”。写出前通知的判定逻辑,并说明为什么必须用“对象身份比较”而不是“路径字符串匹配”(提示:同一路径可能对应多个句柄,路径获取本身需要查询)。
问题 3(独立实现题): 实现进程保护驱动的前回调与卸载注销:1)写出 OnPreOpenProcess——放行内核句柄、查受保护列表、命中则清除 PROCESS_TERMINATE | PROCESS_VM_WRITE;2)写出 OnUnload——按正确顺序注销对象回调与注册表回调、释放列表、删除设备;3)写出用户模式客户下发保护列表的 IOCTL 分发片段(接收 PID 数组、更新列表、返回成功)。要求包含 KernelHandle 检查与注销判空。
知识点对照
9.2 进程保护驱动程序
进程保护驱动程序用对象通知回调拦截句柄打开、复制与关闭操作,从而保护指定的进程不被强行结束。
9.3.1 处理操作前通知
处理操作前通知检查操作类型与目标对象身份,据此决定放行、拒绝或修改请求参数内容,也可直接返回错误。
9.3.2 处理操作后回调
处理操作后回调在操作完成后执行,常用于记录或审计注册表键值的变更,且不能拒绝操作,只能观察结果。
9.4 实现注册表通知
实现注册表通知需要注册回调并维护上下文信息,驱动卸载时必须注销回调并释放相关资源,避免悬垂指针。
9.4.1 处理注册表回调
处理注册表回调按 REG_NOTIFY_CLASS 消息类型分发,分别处理查询、设置与删除等各类操作。
9.4.2 修改后的客户代码
修改后的客户代码增加策略配置功能,把保护名单通过 IOCTL 下发给驱动,并定期刷新名单。
9.2.1 对象通知注册
对象通知注册用 ObRegisterCallbacks 指定操作前后回调并获得注册句柄供后续注销。
9.2.2 管理受保护的进程
管理受保护的进程用链表记录进程 ID 名单,通过客户程序下发的 IOCTL 动态增删查改条目。
9.2.4 客户应用
客户应用通过 DeviceIoControl 下发保护名单规则,驱动据此启用或关闭指定目标进程的保护。
9.3.3 性能考虑
性能考虑要求注册表回调快速返回,避免在回调中做耗时操作、阻塞调用或大量的内存分配,这也是本章的核心要点。
9.6 总结
本章总结了对象通知与注册表通知的注册、回调处理与保护驱动的完整实现要点、流程和注意事项,这也是本章的核心要点。
术语表
名词解释
本章出现的专业名词,用大白话再讲一遍。
- 对象通知
内核在句柄被打开、复制或关闭时调用驱动注册的回调机制,用 ObRegisterCallbacks 注册。相当于大楼门口的安检岗:任何访客要接近目标(打开句柄),都得从这道门过,还能领到一张“上岗证”(注册句柄),撤岗时凭它注销。
- 操作前回调
句柄操作真正发生之前调用的回调,可以检查请求的权限并修改(收窄),或直接拒绝。相当于保安在访客进门时查证件、收走危险物品——拦是它的职责,但它不能等待,只能快速查表做决定。
- 操作后回调
句柄操作完成之后调用的回调,只能观察结果、登记审计,不能改变任何东西。相当于门口的监控摄像头:人进去了就回不来了,监控只负责记一笔“谁进了哪间房”。
- 注册表通知
内核在每次注册表读写、删键、改名等操作时调用的回调,用 CmRegisterCallbackEx 注册。一张回调按操作分类(REG_NOTIFY_CLASS)分流:前通知可以拒绝,后通知只能读结果。相当于档案室的登记岗:查档改档都要过岗,改前可拦、改后留痕。
- 用户模式客户
通过 IOCTL 向驱动下发保护策略(受保护进程列表)、读取审计记录的用户程序。策略是数据不是代码:驱动只认列表不认人,客户程序负责“决定保护谁”,驱动负责“执行拦截与记录”。