17、内核对象管理:对象类型、句柄表、对象引用计数、对象回调、对象安全描述符
内核对象管理,说白了就是Windows内核的“资产管理系统”。
你写的驱动,不管是操作设备、读写文件,还是管理进程,本质上都是在跟内核对象打交道。我早年刚接触驱动开发时,总觉得这玩意儿就是个“黑盒”——CreateFile返回个句柄,然后对着句柄操作就行了。直到有一次,我写的一个过滤驱动莫名其妙蓝屏,查了三天才发现是对象引用计数没处理好。
嗯,从那以后,我老老实实把对象管理这块啃了一遍。今天咱们就把它讲透。
17.1 对象类型:内核里的“类”
Windows内核里,每个对象都有个“身份证”——对象类型(Object Type)。
你可以把它理解成C++里的类。进程是一种类型,线程是一种类型,文件、事件、互斥体……各有各的类型。每种类型都定义了自己的行为:怎么创建、怎么关闭、怎么删除。
对象类型由 OBJECT_TYPE 结构体描述,关键字段如下:
| 字段 | 含义 |
|---|---|
| Name | 类型名称,比如 "Process"、"File" |
| DefaultObject | 默认对象指针(某些类型有) |
| TypeInfo | 类型信息,包含各种操作函数指针 |
其中 TypeInfo 是个 OBJECT_TYPE_INITIALIZER 结构,里面定义了:
- DeleteProcedure:对象销毁时调用的回调
- CloseProcedure:句柄关闭时调用的回调
- SecurityProcedure:安全检查回调
- QueryNameProcedure:查询对象名称的回调
核心要点:每种对象类型都有一组“虚函数表”。内核通过这个表来统一管理所有对象,而不需要关心具体类型。
我个人习惯在开发自定义对象类型时,把 DeleteProcedure 和 CloseProcedure 写得特别小心。为什么?因为这两个回调一旦写错,轻则内存泄漏,重则直接蓝屏。
17.2 句柄表:进程的“对象抽屉”
每个进程都有一个句柄表(Handle Table)。
句柄,本质上就是一个索引值。你拿着这个索引,去句柄表里查,就能找到对应的内核对象指针。
句柄表的结构大致如下:
// 句柄表项(简化版)
typedef struct _HANDLE_TABLE_ENTRY {
union {
PVOID Object; // 指向内核对象的指针
ULONG32 ObAttributes; // 对象属性(低3位是标志位)
HANDLE_TABLE_ENTRY_INFO *InfoTable;
};
ULONG32 GrantedAccessBits; // 授予的访问权限
} HANDLE_TABLE_ENTRY, *PHANDLE_TABLE_ENTRY;
注意看,Object 指针的低3位被用来存标志位了。这是内核常用的“偷位”技巧——反正指针低3位在64位系统上永远是0,不用白不用。
句柄表本身是个多级表,类似页表的结构:
- 一级表:直接映射(小进程)
- 二级表:中等规模
- 三级表:大进程(比如跑了几百个线程的浏览器)
实战技巧:遍历句柄表时,别用 ZwQuerySystemInformation 的 SystemHandleInformation——那个返回的是全局句柄信息,性能很差。我一般用 ExpLookupHandleTableEntry 配合进程的 ObjectTable 来查。
17.3 对象引用计数:别让对象“死”早了
每个内核对象都有一个引用计数(Reference Count)。
引用计数归零时,对象就会被销毁。这个机制很简单,但坑很多。
引用计数操作主要有两个函数:
ObReferenceObject:增加引用计数ObDereferenceObject:减少引用计数
我遇到过最经典的bug是这样的:
// 错误示例:引用计数失衡
void BadFunction() {
PDEVICE_OBJECT deviceObj = ...;
ObReferenceObject(deviceObj); // 引用+1
// ... 做一些操作 ...
// 忘记调用 ObDereferenceObject!
// 设备对象永远无法被卸载
}
这种bug不会立刻蓝屏,但会导致驱动卸载不掉。你想想看,用户插拔设备时,系统发现引用计数不为0,直接拒绝卸载——用户就卡在那了。
避坑指南:我曾经在一个项目里,因为某个回调路径上多了一次 ObReferenceObject,导致设备对象泄漏。查了整整两天,最后用 !object 调试命令才发现引用计数多了1。
建议:每次 ObReferenceObject 都配一个 ObDereferenceObject,最好写在同一个函数里,或者用 __try/__finally 保证成对执行。
17.4 对象回调:监控对象的“摄像头”
从Windows Vista开始,内核提供了对象回调机制(Object Callbacks)。
你可以注册回调函数,监控进程、线程、桌面等对象的创建和复制操作。这个功能在安全软件、监控工具里用得特别多。
注册回调的API:
NTSTATUS ObRegisterCallbacks(
POB_CALLBACK_REGISTRATION CallbackRegistration,
PVOID *RegistrationHandle
);
回调结构体:
typedef struct _OB_CALLBACK_REGISTRATION {
USHORT Version; // 必须为 OB_FLT_REGISTRATION_VERSION
USHORT OperationRegistrationCount;
UNICODE_STRING Altitude; // 海拔高度,决定回调顺序
PVOID RegistrationContext;
OB_OPERATION_REGISTRATION OperationRegistration[];
} OB_CALLBACK_REGISTRATION, *POB_CALLBACK_REGISTRATION;
每个 OB_OPERATION_REGISTRATION 指定了:
- 监控的对象类型(如 PsProcessType、PsThreadType)
- 监控的操作(创建、复制)
- 回调函数指针
注意:回调函数运行在APC级别(APC_LEVEL),不能做太多耗时操作。我见过有人直接在回调里申请大内存,结果系统性能直接崩了。
回调函数原型:
OB_PREOP_CALLBACK_STATUS PreOperationCallback(
PVOID RegistrationContext,
POB_PRE_OPERATION_INFORMATION OperationInformation
);
你可以在这里决定:
- 允许操作:返回
OB_PREOP_SUCCESS - 拒绝操作:返回
OB_PREOP_ERROR并设置错误状态 - 继续监控:返回
OB_PREOP_SUCCESS并设置后置回调
17.5 对象安全描述符:谁可以碰这个对象?
每个内核对象都可以关联一个安全描述符(Security Descriptor)。
安全描述符定义了:谁(SID)能对这个对象做什么(访问掩码)。
安全描述符的结构:
typedef struct _SECURITY_DESCRIPTOR {
UCHAR Revision;
UCHAR Sbz1;
SECURITY_DESCRIPTOR_CONTROL Control;
PSID Owner; // 所有者SID
PSID Group; // 主组SID
PACL Sacl; // 系统ACL(审计用)
PACL Dacl; // 自由ACL(权限控制)
} SECURITY_DESCRIPTOR, *PISECURITY_DESCRIPTOR;
DACL(自由访问控制列表)是核心。它包含一组ACE(访问控制项),每个ACE指定:
- 允许还是拒绝
- 针对哪个用户/组
- 哪些操作权限
举个例子:
// 创建一个只允许SYSTEM访问的设备对象
void CreateSecureDevice() {
UNICODE_STRING deviceName;
RtlInitUnicodeString(&deviceName, L"\\Device\\MySecureDevice");
SECURITY_DESCRIPTOR sd;
InitializeSecurityDescriptor(&sd, SECURITY_DESCRIPTOR_REVISION);
SID_IDENTIFIER_AUTHORITY ntAuthority = SECURITY_NT_AUTHORITY;
PSID systemSid;
AllocateAndInitializeSid(&ntAuthority, 1, SECURITY_LOCAL_SYSTEM_RID,
0, 0, 0, 0, 0, 0, 0, &systemSid);
EXPLICIT_ACCESS ea;
RtlZeroMemory(&ea, sizeof(ea));
ea.grfAccessPermissions = GENERIC_ALL;
ea.grfAccessMode = GRANT_ACCESS;
ea.Trustee.TrusteeForm = TRUSTEE_IS_SID;
ea.Trustee.ptstrName = (LPTSTR)systemSid;
PACL dacl;
SetEntriesInAcl(1, &ea, NULL, &dacl);
SetSecurityDescriptorDacl(&sd, TRUE, dacl, FALSE);
// 创建设备时指定安全描述符
// ...
}
我的经验:很多驱动开发者图省事,直接传NULL安全描述符。这意味着所有用户都能访问你的设备——这在生产环境里是个大坑。我建议至少给设备对象设置一个最小权限的DACL,只允许SYSTEM和Administrators访问。
17.6 知识体系总览
下面这张图把内核对象管理的核心脉络串起来了:
17.7 实战要点总结
讲了这么多,我总结几个实战中必须记住的点:
- 对象类型是静态的——系统启动时就注册好了,驱动可以注册自己的类型,但别乱改系统类型。
- 句柄表是进程私有的——A进程的句柄不能直接在B进程用,除非通过
DuplicateHandle。 - 引用计数必须成对——多一次
ObReferenceObject就多一次泄漏,少一次就蓝屏。 - 回调里别睡——APC_LEVEL下不能等事件、不能申请分页内存,否则系统直接崩给你看。
- 安全描述符别偷懒——NULL安全描述符等于“大门敞开”,生产环境一定要设权限。
最后提醒:调试对象相关问题时,Windbg的 !object、!handle、!process 命令是你的好帮手。我每次遇到句柄泄漏,第一件事就是 !handle 0 0 Process 看看哪个进程的句柄数异常。
好了,内核对象管理这块就讲到这里。内容不少,但都是驱动开发绕不开的基础。你写驱动时,多想想“这个对象的引用计数对不对”、“安全描述符设了没”,能省下不少调试时间。
公众号:蓝海资料掘金营,微信deep3321