上周同事的驱动遇到HLK测试失败:HLK测试项检测到传感器设备驱动在响应IRP_MJ_PNP/IRP_MN_REMOVEDEVICE时,有句柄扔打开设备,于是我也帮着一起查找原因。这个驱动的架构如下设计:上层App负责通知驱动,驱动响应这个过程时,用IoGetDeviceObjectPointer获得并保持(IoGetDeviceObjectPointer后没有对返回的文件对象调用ObDerefernceObject)传感器设备对象;直到App通知驱动停止时,驱动才对文件对象进行解引用。看到如此流程,又因为这份驱动年代久远,期间又经由多人之手(简单的说就是没有spec),我初步断定就是因为文件对象一直没有解引用引起的HLK测试项失败。后来,经另一位同事提醒,当时的设计背景可能是考虑到传感器在使用过程中可能会被停用,为了保证传感器对象的有效性,原作者在驱动工作期间保持该对象的引用计数,这样,传感器对象至少在我们的驱动主动停用前不会被卸载。
不太相关的背景交代完毕,现在回到题目。开始时,我看
IoGetDeviceObjectPointer返回设备对象和文件对象,以为这个函数会使目标设备的引用计数+2,调用ObDereferenceObject(FileObject);后,目标设备上的引用计数为1,所以总觉得还需要对设备对象进行一次解引用才能彻底释放。因为有这样的疑惑,我没有马上动手,而是写了一份测试代码调试了IoGetDeviceObjectPointer对指定设备的影响。我用的测试设备是DDK源码src\general\ioctl编译生成的sioctl设备;测试IoGetDeviceObjectPointer对sioctl的引用的源码如下:
#define NT_DEVICE_NAME L"\\Device\\SIOCTL"
#define DOS_DEVICE_NAME L"\\DosDevices\\IoctlTest"
NTSTATUS
DriverEntry(
__in PDRIVER_OBJECT DriverObject,
__in PUNICODE_STRING RegistryPath
)
{
UNICODE_STRING devStr;
NTSTATUS ntStatus;
FILE_OBJECT* fileObj;
DEVICE_OBJECT* devObj;
RtlInitUnicodeString(&devStr, DOS_DEVICE_NAME);
ntStatus = IoGetDeviceObjectPointer(&devStr, FILE_ALL_ACCESS, &fileObj, &devObj);
if(ntStatus == STATUS_SUCCESS)
ObDereferenceObject(fileObj);
return ntStatus;
}
下面是我的调试过程以我对调试输出的个人理解:
kd> g
KDTARGET: Refreshing KD connection
Breakpoint 0 hit
getdev!DriverEntry:
92ee1010 8bff mov edi,edi
驱动编译后模块名为getdev,我对DriverEntry下了延迟断点,这就可以观察调用IoGetDeviceObjectPointer前sioctl的引用计数:
kd> t
nt!IoGetDeviceObjectPointer:
82ca2e98 8bff mov edi,edi
kd> !drvobj sioctl
Driver object (850ff468) is for:
\Driver\sioctl
Driver Extension List: (id , addr)
Device Object list:
850ff340
kd> !devobj 850ff340
Device object (850ff340) is for:
SIOCTL \Driver\sioctl DriverObject 850ff468
Current Irp 00000000 RefCount 0 <------引用计数为0 Type 00000022 Flags 00000040
...
kd> !object 850ff340
Object: 850ff340 Type: (84ff7bc8) Device
ObjectHeader: 850ff328 (new version)
HandleCount: 0 PointerCount: 2 ;<-------对象指针计数==2
Directory Object: 88a0f710 Name: SIOCTL
刚进入IoGetDeviceObjectPointer时,sioctl引用计数等于0,另外它的对象指针计数为2,这是初始值。
查看
IoGetDeviceObjectPointer的源码,第一印象有4个函数可能会影响sioctl的引用计数,他们分别是:ZwOpenFile,ObReferenceObjectByHandle,IoGetRelatedDeviceObject,ZwClose.
NTSTATUS
NTAPI
IopGetDeviceObjectPointer(IN PUNICODE_STRING ObjectName,
IN ACCESS_MASK DesiredAccess,
OUT PFILE_OBJECT *FileObject,
OUT PDEVICE_OBJECT *DeviceObject,
IN ULONG AttachFlag)
{
InitializeObjectAttributes(&ObjectAttributes,
ObjectName,
OBJ_KERNEL_HANDLE,
NULL,
NULL);
Status = ZwOpenFile(&FileHandle,
DesiredAccess,
&ObjectAttributes,
&StatusBlock,
0,
FILE_NON_DIRECTORY_FILE | AttachFlag);
if (!NT_SUCCESS(Status)) return Status;
Status = ObReferenceObjectByHandle(FileHandle,
0,
IoFileObjectType,
KernelMode,
(PVOID*)&LocalFileObject,
NULL);
if (NT_SUCCESS(Status))
{
*DeviceObject = IoGetRelatedDeviceObject(LocalFileObject);
*FileObject = LocalFileObject;
ZwClose(FileHandle);
}
return Status;
}
我要做的就是在进出这些函数前后查看sioctl的对象指针及引用计数的变化,所以要在这些入口下断点。另外,在x86的机器上,设备对象的RefCount字段距对象头的偏移是4B,可以在这个位置下访问断点:
kd> u 82ca2eec
82ca2eec e89fa8dbff call nt!ZwOpenFile (82a5d790)
kd> u 82ca2f09
82ca2f09 e8f7d1f9ff call nt!ObReferenceObjectByHandle (82c40105)
kd> u 82ca2f1e
82ca2f1e e837c1e2ff call nt!IoGetRelatedDeviceObject (82acf05a)
kd> u 82ca2f2c
82ca2f2c e84b9edbff call nt!ZwClose (82a5cd7c)
;普通断点
kd> bp 82ca2eec
kd> bp 82ca2f09
kd> bp 82ca2f1e
kd> bp 82ca2f2c
;访问断点
kd> ba w 4 850ff340 +4
kd> bl
0 e Disable Clear 92ee1010 [c:\users\eugene\desktop\studio\ioget\getdev.c @ 12] 0001 (0001) getdev!DriverEntry
1 e Disable Clear 92ee1041 [c:\users\eugene\desktop\studio\ioget\getdev.c @ 21] 0001 (0001) getdev!DriverEntry+0x31
2 e Disable Clear 82ca2eec 0001 (0001) nt!IoGetDeviceObjectPointer+0x54
3 e Disable Clear 82ca2f09 0001 (0001) nt!IoGetDeviceObjectPointer+0x71
4 e Disable Clear 82ca2f1e 0001 (0001) nt!IoGetDeviceObjectPointer+0x86
5 e Disable Clear 82ca2f2c 0001 (0001) nt!IoGetDeviceObjectPointer+0x94
6 e Disable Clear 850ff344 w 4 0001 (0001)
好了,准备工作做好了,开始我的探索之旅:
kd> g
Breakpoint 6 hit
nt!IopCheckDeviceAndDriver+0x45:
82ab8393 33f6 xor esi,esi
kd> kb
# ChildEBP RetAddr Args to Child
00 807e5750 82c5b889 19870092 807e58f8 00000000 nt!IopCheckDeviceAndDriver+0x45
01 807e5828 82c3d1d7 850ff340 84ff7970 87248008 nt!IopParseDevice+0x133
02 807e58a4 82c6324d 00000000 807e58f8 00000240 nt!ObpLookupObjectName+0x4fa
03 807e5904 82c5b5ab 807e5a90 84ff7970 00022000 nt!ObOpenObjectByName+0x159
04 807e5980 82c965ee 807e5a80 001f01ff 807e5a90 nt!IopCreateFile+0x673
05 807e59c8 82a5f42a 807e5a80 001f01ff 807e5a90 nt!NtOpenFile+0x2a
06 807e59c8 82a5d7a1 807e5a80 001f01ff 807e5a90 nt!KiFastCallEntry+0x12a
07 807e5a58 82ca2ef1 807e5a80 001f01ff 807e5a90 nt!ZwOpenFile+0x11
08 807e5aac 92ee103e 807e5acc 001f01ff 807e5ad4 nt!IoGetDeviceObjectPointer+0x59
09 807e5ad8 82bbf728 8514fea8 85141000 00000000 getdev!DriverEntry+0x2e [c:\users\eugene\desktop\studio\ioget\getdev.c @ 19]
...
首次触发的断点是访问断点,
ZwOpenFile打开sioctl设备的过程中,OS对sioctl的引用计数做了加1操作,我们可以在ZwOpenFile返回后验证:
kd> g
Breakpoint 3 hit
nt!IoGetDeviceObjectPointer+0x71:
82ca2f09 e8f7d1f9ff call nt!ObReferenceObjectByHandle (82c40105)
kd> kb
# ChildEBP RetAddr Args to Child
00 807e5aac 92ee103e 807e5acc 001f01ff 807e5ad4 nt!IoGetDeviceObjectPointer+0x71
01 807e5ad8 82bbf728 8514fea8 85141000 00000000 getdev!DriverEntry+0x2e [c:\users\eugene\desktop\studio\ioget\getdev.c @ 19]
...
kd> r esp
esp=807e5a60
kd> !handle poi(@esp) 7
PROCESS 84fe5a20 SessionId: none Cid: 0004 Peb: 00000000 ParentCid: 0000
DirBase: 00185000 ObjectTable: 88a01b40 HandleCount: 480.
Image: System
Kernel handle table at 88a01b40 with 480 entries in use
800007f8: Object: 85153d58 GrantedAccess: 001f01ff Entry: 88a03ff0
Object: 85153d58 Type: (84ff7970) File
ObjectHeader: 85153d40 (new version)
HandleCount: 1 PointerCount: 1
kd> !fileobj 85153d58
Device Object: 0x850ff340 \Driver\sioctl
Vpb is NULL
Event signalled
Flags: 0x40000
Handle Created
CurrentByteOffset: 0
ZwOpenFile打开sioctl的文件对象,并返回文件对象的句柄。我们可以简单的认为句柄直到ZwOpenFile函数返回才有效。当获得有效句柄后,就能用!handle调试命令获得句柄对应的对象及对象状态。现在的问题是从哪里能获得ZwOpenFile返回的句柄?查看IoGetDeviceObjectPointer的源码发现,ObReferenceObjectByHandle会用到这个句柄。问题变得很简单了,在执行
82ca2f09 e8f7d1f9ff call nt!ObReferenceObjectByHandle (82c40105)
前,OS已经准备好了必要的参数,所以,只要从堆栈上获得参数就行了。此时堆栈顶由esp指向,所以我从esp处取出了sioctl句柄,正是这句:
kd> !handle poi(@esp) 7
windbg分析认为传入的是指向文件对象的句柄,文件对象的句柄计数和对象计数分别是1。
结合<深入解析Windows操作系统>知,句柄数增加和对象数增加是一一对应的,每次OS打开返回一个句柄,必然在内核中准备了一个对象。
前文曽怀疑调用ObReferenceObjectByHandle后,会影响sioctl的引用计数。现在来验证猜想是否正确。
kd> p
nt!IoGetDeviceObjectPointer+0x76:
82ca2f0e 8bf8 mov edi,eax
kd> ub . L2
nt!IoGetDeviceObjectPointer+0x6d:
82ca2f05 ff74241c push dword ptr [esp+1Ch]
82ca2f09 e8f7d1f9ff call nt!ObReferenceObjectByHandle (82c40105)
kd> !object 85153d58
Object: 85153d58 Type: (84ff7970) File
ObjectHeader: 85153d40 (new version)
HandleCount: 1 PointerCount: 2
上面的调试记录,我用p单步越过ObReferenceObjectByHandle,然后再查看对象状态(!object 85153d58查看的就是ZwOpenFile返回的句柄所指向的文件对象的状态)。windbg分析出,自调用ObReferenceObjectByHandle后,对象的句柄计数保持为1,而指针对象增加到2。
由于,内核除了可以使用句柄,还能单独打开并使用句柄背后的对象。所以在内核中,同一个对象的记录的PointerCount>=HandlerCount。
前面执行ZwOpenFile,返回了sioctl设备对应的文件对象,所以暂时没有影响设备对象的PointerCount。但接下来马上会调用IoGetRelatedDeviceObject----从文件对象获得设备对象----这是不是会增加设备对象的PointerCount?继续调试并验证这个猜想。先看下调用IoGetRelatedDeviceObject前设备对象的各种计数:
kd> g
Breakpoint 4 hit
nt!IoGetDeviceObjectPointer+0x86:
82ca2f1e e837c1e2ff call nt!IoGetRelatedDeviceObject (82acf05a)
kd> ub . L2
nt!IoGetDeviceObjectPointer+0x83: ;IoGetRelatedDeivceObject的参数是FileObject,结合反汇编可知,eax中保存了FileObject
82ca2f1b 50 push eax
82ca2f1c 8901 mov dword ptr [ecx],eax
kd> r eax
eax=85153d58
kd> !object 850ff340
Object: 850ff340 Type: (84ff7bc8) Device
ObjectHeader: 850ff328 (new version)
HandleCount: 0 PointerCount: 2
Directory Object: 88a0f710 Name: SIOCTL
!object 850ff340
上面命令用于查看设备对象(0x850ff340是设备对象的地址)状态的命令。
1).刚进入IoGetDeviceObjectPointer时执行过这条命令;
2).调用了IoGetRelatedDeviceObject,并从FileObject成功获得DeviceObject后,再次执行这条命令;
对比1),2)处两次命令执行,除了设备对象的RefCount不同以外,其他输出一致。侧面反映了在IoGetDeviceObjectPointer中执行ZwOpenFile和ObReferenceObjectByHandle以及IoGetRelatedDeviceObject函数并不会影响设备对象的HandleCount和PointerCount的值。
有点奇怪,IoGetRelatedDeviceObject明明用于获得设备对象,可是却没有修改PointerCount的值,为什么会这样?答案在源码中:
PDEVICE_OBJECT
NTAPI
IoGetRelatedDeviceObject(IN PFILE_OBJECT FileObject)
{
PDEVICE_OBJECT DeviceObject = FileObject->DeviceObject;
...
else
{
/* Otherwise, this was a direct open */
DeviceObject = FileObject->DeviceObject;
}
...
/* Check if we were attached */
if (DeviceObject->AttachedDevice)
{
/* Check if the file object has an extension present */
if (FileObject->Flags & FO_FILE_OBJECT_HAS_EXTENSION)
{
/* Sanity check, direct open files can't have this */
ASSERT(!(FileObject->Flags & FO_DIRECT_DEVICE_OPEN));
/* Check if the extension is really present */
if (FileObject->FileObjectExtension)
{
/* FIXME: Unhandled yet */
DPRINT1("FOEs not supported\n");
KEBUGCHECK(0);
}
}
/* Return the highest attached device */
DeviceObject = IoGetAttachedDevice(DeviceObject);
}
/* Return the DO we found */
return DeviceObject;
}
因为FileObject中存有DeviceObject的指针,并没有经过对象管理器分配DeviceObject对象,所以不会增加sioctl的PointerCount。
在IoGetDeviceObjectPointer的尾部,执行ZwClose关闭句柄。毋庸置疑,这一定会减少FileObject的HandleCount。前面说过,HandleCount和PoniterCount是一一对应的关系,HandleCount减少,必然是PointerCount减少引起的。所以,可以确定FileObject的PointerCount也减少了。有调试输出为证:
kd> g
Breakpoint 5 hit
nt!IoGetDeviceObjectPointer+0x94:
82ca2f2c e84b9edbff call nt!ZwClose (82a5cd7c)
kd> r esp
esp=807e5a74 ;调用ZwClose前,栈顶保存了ZwClose的参数,也就是句柄
kd> !handle poi(@esp)
...
800007f8: Object: 85153d58 GrantedAccess: 001f01ff Entry: 88a03ff0
Object: 85153d58 Type: (84ff7970) File
ObjectHeader: 85153d40 (new version)
HandleCount: 1 PointerCount: 2
;调用ZwClose前HandleCount=1,PointerCount=2
kd> p ;Step Over ZwClose
nt!IoGetDeviceObjectPointer+0x99:
82ca2f31 8bc7 mov eax,edi
;调用ZwClose后,句柄无效,所以不再使用!handle命令;但是FileObject仍然存在,因此还能用!object来查看FileObject状态
kd> !object 85153d58
Object: 85153d58 Type: (84ff7970) File
ObjectHeader: 85153d40 (new version)
HandleCount: 0 PointerCount: 1
当IoGetDeviceObjectPointer返回到示例驱动中,sioctl对应的FileObject!pointerCount保持为1,sioctl的RefCount同样为1:
kd> !devobj 850ff340 ;850ff340是sioctl的设备对象地址
Device object (850ff340) is for:
SIOCTL \Driver\sioctl DriverObject 850ff468
Current Irp 00000000 RefCount 1 ;<----------------------引用计数仍为1
SecurityDescriptor 88a51678 DevExt 00000000 DevObjExt 850ff3f8
ExtensionFlags (0x00000800) DOE_DEFAULT_SD_PRESENT
Characteristics (0x00000100) FILE_DEVICE_SECURE_OPEN
Device queue is not busy.
kd> !object 850ff340
Object: 850ff340 Type: (84ff7bc8) Device
ObjectHeader: 850ff328 (new version)
HandleCount: 0 PointerCount: 2 ; <-----------HandleCount/PointerCount稳定保持不变,呵呵,真是波澜不惊
Directory Object: 88a0f710 Name: SIOCTL
难道sioctl的引用计数永远锁定在1了?岂不是泄露了?倒也不至于,只要对FileObject执行一次解引用(ObDereferenceObject),sioctl!RefCount就归零了:
kd> g
Breakpoint 1 hit ;断点1是测试代码中的ObDereferenceObject语句
getdev!DriverEntry+0x31:
92ee1041 837dec00 cmp dword ptr [ebp-14h],0
kd> p
getdev!DriverEntry+0x37:
92ee1047 8b4dfc mov ecx,dword ptr [ebp-4]
kd> p ;单步越过ObDereferenceObject后,还会触发访问断点,即sioctl设备对象的引用计数被清零了
Breakpoint 6 hit
nt!IopDecrementDeviceObjectRef+0x12:
82ac39d2 8ad0 mov dl,al
kd> !object 0x85153d58 ;再次查看FileObject,发现pointerCount终于清零了
Object: 85153d58 Type: (84ff7970) File
ObjectHeader: 85153d40 (new version)
HandleCount: 0 PointerCount: 0
kd> g
Breakpoint 2 hit
参考资料:<深入理解Windows操作系统>P141-142 对象保持力