第9章 对象和注册表通知

用对象操作前回调拦截句柄权限实现进程保护,用操作后回调登记审计,再实现注册表前后通知与卸载注销的完整纪律。

学习目标

  • 能解释 ObRegisterCallbacks 的注册结构(对象类型、操作类别、海拔)与前/后回调的分工
  • 能改出“受保护进程列表 + 前回调拦截权限 + 后回调登记审计”的进程保护驱动,并在卸载时正确注销
  • 能回答:为什么拦截必须发生在操作前回调里,操作后回调只能观察结果?

为什么大楼门口要装安检,档案室要装登记岗

想象一栋大楼:访客要进楼(程序想打开某个进程),保安在门口查证件——证件不合规,当场拦下;人进去之后,监控只负责记录“谁进了哪间房”,不可能再把进去的人拽出来。“拦”和“记”是两件必须分开的事。大楼里还有一间档案室(注册表),谁的档案被翻看、被改动,都要过登记岗:改动前登记岗可以拒绝,改动后只能留一笔记录。

没有这套系统会怎样?任何程序都能随意打开受保护进程,安全软件只能干瞪眼;注册表被偷偷改掉,没人知道、也没人拦得住。

这一章装这两套系统:大楼门口的安检(对象通知)和档案室的登记岗(注册表通知),最后学会安全撤岗(卸载注销)——撤岗的纪律和装岗一样重要。

对象通知:给句柄操作装一道安检门

让你在“获得对象句柄”这条必经之路上设卡。注册时要声明三件事:监控哪类对象(进程 PsProcessType、线程 PsThreadType)、哪些操作(OB_OPERATION_HANDLE_CREATE 创建、OB_OPERATION_HANDLE_DUPLICATE 复制)、以及海拔(Altitude,一串字符串,决定你的回调排在别的回调之前还是之后)。

注册成功会返回一个注册句柄(Cookie)——这是你的“上岗证”,卸载时必须用它注销。它的位置在句柄操作路径上:任何进程想打开受保护进程的句柄,都要先过这道门。

操作前回调:在门口把权限拦下来

是进程保护的拦截点:查受保护列表 → 命中 → 清除危险权限。注意是“清除权限”而不是“直接拒绝”——系统里有些组件确实需要拿到受限句柄,一律拒绝会把系统搞崩。

回调返回后句柄按修改过的权限继续创建,所以真正落地的权限是你改过的版本。内核自己打开的句柄(KernelHandle 字段)必须放行:拦截内核会让系统无法启动。这个回调里只能查自旋锁保护的数据、做位运算,不能等待。

操作后回调:记一笔审计

和门口安检正好互补:人已经进去了,监控补记一笔——谁(当前进程 ID)、什么时间、对哪个对象、要了什么权限、是创建还是复制。

它不能拒绝任何操作(操作已经发生),也不该做重活。前回调拦不拦是策略,后回调记不记是证据。安全产品的“谁碰过我的进程”审计,就是从这里来的。

注册表通知:档案室的登记岗

覆盖全部注册表操作。回调的第一个参数是操作分类(REG_NOTIFY_CLASS):RegNtSetValueKey(写值之前)、RegNtPostSetValueKey(写值之后)、RegNtDeleteKey(删键之前)……前通知可以拒绝(返回 STATUS_ACCESS_DENIED),后通知只能读到操作结果。

回调运行在 PASSIVE_LEVEL,但注册表操作极高频——回调必须短小,只做“查键路径 + 比较 + 返回”,不能打开文件、不能等锁。注册表过滤是安全产品的基础设施,做慢了整个系统变卡。

用户模式客户:策略从哪来

回答“保护谁”的问题:受保护进程列表不该硬编码在驱动里,由客户程序通过 IOCTL 下发,驱动维护列表,前回调查表拦截;审计记录同样由客户程序取走。

配置通道低频小包(改一次列表),通知通道高频批量(审计记录),与第 8 章的通知管道同构。驱动侧永远只认列表不认人——策略是数据,不是代码。

动手:走一遍对象与注册表通知

下面的可视化把两套系统画成一张图:上半部分是对象通知——选“打开句柄/复制句柄”,看前回调怎么检查、后回调怎么审计;打开“进程保护”开关后再点“打开句柄”,观察拦截效果。下半部分是注册表通知——选查询/设置/删除,看前通知与后通知的分流。

猜一猜:打开“进程保护”开关后,再点击“打开句柄”——前回调框会变成什么颜色?操作结果条会显示什么?后回调框为什么显示“未执行”?

⚡ 对象通知与注册表通知工作台
对象通知:句柄操作的门口检查 · 注册表通知:操作流水对象通知(ObRegisterCallbacks)打开句柄复制句柄PreOperation 前回调检查访问掩码 → 允许 / 拒绝放行:操作继续PostOperation 后回调登记审计:谁 / 何时 / 对哪个对象操作结果 = 成功 → 记一笔进程保护关闭:所有打开操作放行注册表通知(CmRegisterCallbackEx)查询键值RegNtQueryValueKey设置键值RegNtSetValueKey删除键RegNtDeleteKey前通知:RegNtSetValueKey检查键路径与操作类型 → 可拒绝敏感操作回调必须短小:注册表操作高频,慢了系统卡顿后通知:操作结果读取最终 Status:成功 / 失败只观察不拦截 → 审计日志用用户模式客户:IOCTL 配置受保护列表 / 读取审计记录配置通道低频小包 · 通知通道高频批量 · 与第 8 章管道同构当前模拟:对象操作「打开句柄」+ 注册表操作「设置键值」均放行
操作前回调(PreOperation)

操作前回调在句柄操作真正发生之前调用,可以:检查访问掩码(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(&reg, &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;
}

段三:后回调审计 —— 只记录,不拦截

操作完成后内核再调一次后回调:把“谁、对哪个进程、什么操作、要了什么权限”写进审计缓冲。注意它拿到的权限是请求的权限(可能已被前回调收窄,也可能本来就是受限请求)——审计的职责是如实记录请求,而不是判断好坏。

// 操作后回调:只记录,不拦截
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);                       // 要了什么权限
}

段四:注册表回调 —— 一张回调,两种身份

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;
}

段五:卸载注销 —— 撤岗的顺序不能乱

卸载是整章纪律的总验收:先注销对象回调(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);
}

容易踩的坑

小结

  • 对象通知经 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 向驱动下发保护策略(受保护进程列表)、读取审计记录的用户程序。策略是数据不是代码:驱动只认列表不认人,客户程序负责“决定保护谁”,驱动负责“执行拦截与记录”。

讨论

评论区加载中…