热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

Ring3调用NtQueryObject获得文件句柄对应的对象名时调用线程死锁的原因

之前遗留的一个问题http:blog.csdn.netqq_18218335articledetails76400680之前实现Ring3查找文件占用的时候发现对部分句柄进行Nt

之前遗留的一个问题

http://blog.csdn.net/qq_18218335/article/details/76400680
        之前实现Ring3 查找文件占用的时候发现对部分句柄进行NtQueryObject 操作的时候会造成调用者线程挂起,从Ring3 解决问题的方法就是生成一个单独的工作线程,代替我们执行NtQueryObject 函数,如果工作线程挂起,则调用KillThread 将其结束并新生成一个工作线程,这种方法虽然可以解决死锁问题,但是无法得到对象的所有信息。之后我们通过驱动调用ObQueryNameString 并与Ring3 交互成功得到了所有句柄对应的名称信息。这篇文章就来分析为什么同样是调用系统提供的接口函数,Ring3 会导致线程死锁,而Ring0 则可以直接得到对象名称。


实验环境

        Win7 X64 sp1,wrk,IDA,vmware 12,windbg。vs2015


实验思路

        之前尝试直接通过WinDbg 在NtQueryObject 函数下断点来分析其调用流程,之后发现系统自己多次调用该函数来解析句柄的信息。如果想知道自己的函数调用为什么死锁,必须要hook 该函数,然后再判断是自己的程序进行的函数调用的时候中断系统,之后再进行单步调试,发现线程死锁的原因。整体流程就是,Ring3 首先执行一遍查找操作,判断出谁可能导致线程死锁,之后加载我们的hook 驱动,此时Ring3 程序针对特定的导致死锁的句柄调用NtQueryObject 函数,我们的驱动在捕获此次调用的时候中断系统。此时我们已经得到了依次必然导致线程死锁的函数调用,我们通过WinDbg 单步调试程序,并结合wrk 中已经给出的函数实现来综合分析其死锁原因。
        


1. 找到导致死锁的句柄

这里写图片描述

这里写图片描述


2.编写驱动,捕获我们对于该句柄的NtQueryObject 函数调用

这里写图片描述

这里写图片描述

这里写图片描述


3.综合利用各种工具进行分析

        到这里之后我们没有必要刚开始就一无所知去单步调试汇编,可以借助wrk 源码和 IDA 来分析其行为,之后再单步调试。首先看Wrk 中 NtQueryObject 的实现。

case ObjectNameInformation://// Call a local worker routine//Status = ObpQueryNameString( Object,(POBJECT_NAME_INFORMATION)ObjectInformation,ObjectInformationLength,&TempReturnLength,PreviousMode );break;

         我们看到,当我们要去获得句柄对应的名称信息的时候,函数转而去调用ObpQueryNameString函数。我们来看ObpQueryNameString 的函数实现;

if (ObjectHeader->Type->TypeInfo.QueryNameProcedure != NULL) {try {#if DBGKIRQL SaveIrql;
#endifObpBeginTypeSpecificCallOut( SaveIrql );ObpEndTypeSpecificCallOut( SaveIrql, "Query", ObjectHeader->Type, Object );Status = (*ObjectHeader->Type->TypeInfo.QueryNameProcedure)( Object,(BOOLEAN)((NameInfo != NULL) && (NameInfo->Name.Length != 0)),ObjectNameInfo,Length,ReturnLength,Mode );} except( EXCEPTION_EXECUTE_HANDLER ) {Status = GetExceptionCode();}ObpDereferenceNameInfo( NameInfo );return( Status );}

        从上面给出的代码实现来看,该函数首先查看对象所对应的对象类型的QueryNameProcedure 函数,如果有的话,直接调用该函数,我们看到这里的对象类型为File ,文件类型是肯定有其QueryNameProcedure 的,但是我们需要通过WinDbg 函数验证这个调用过程,并通过WinDbg 找到File 对应的QueryNameProcedure 函数的地址。

这里写图片描述
        单步F10 直到调用ObpQueryNameString ,验证了我们的思路,之后F11 进入函数实现。

nt!ObpQueryNameString:
fffff800`041972f0 488bc4 mov rax,rsp
fffff800`041972f3 4c894820 mov qword ptr [rax+20h],r9
fffff800`041972f7 44894018 mov dword ptr [rax+18h],r8d
fffff800`041972fb 48895010 mov qword ptr [rax+10h],rdx
fffff800`041972ff 48894808 mov qword ptr [rax+8],rcx
fffff800`04197303 53 push rbx
fffff800`04197304 56 push rsi
fffff800`04197305 57 push rdi
fffff800`04197306 4154 push r12
fffff800`04197308 4155 push r13
fffff800`0419730a 4156 push r14
fffff800`0419730c 4157 push r15
fffff800`0419730e 4881ecc0000000 sub rsp,0C0h
fffff800`04197315 4c8bf2 mov r14,rdx
fffff800`04197318 c7442440010000c0 mov dword ptr [rsp+40h],0C0000001h
fffff800`04197320 33ff xor edi,edi
fffff800`04197322 897c2450 mov dword ptr [rsp+50h],edi
fffff800`04197326 48897c2448 mov qword ptr [rsp+48h],rdi
fffff800`0419732b 8d7701 lea esi,[rdi+1]
fffff800`0419732e 4088742431 mov byte ptr [rsp+31h],sil
fffff800`04197333 40887c2430 mov byte ptr [rsp+30h],dil
fffff800`04197338 4c8d79d0 lea r15,[rcx-30h]
fffff800`0419733c 4c897c2470 mov qword ptr [rsp+70h],r15
fffff800`04197341 410fb64718 movzx eax,byte ptr [r15+18h]
fffff800`04197346 4c8d2db33ccbff lea r13,[nt!KiSelectNextThread (nt+0x0) (fffff800`03e4b000)]
fffff800`0419734d 4d8b94c5807b2200 mov r10,qword ptr [r13+rax*8+227B80h]
fffff800`04197355 41f6471a02 test byte ptr [r15+1Ah],2
fffff800`0419735a 0f853b090000 jne nt!ObpQueryNameString+0x9ab (fffff800`04197c9b)
fffff800`04197360 488bdf mov rbx,rdi
fffff800`04197363 48895c2468 mov qword ptr [rsp+68h],rbx
fffff800`04197368 4d8b92a0000000 mov r10,qword ptr [r10+0A0h]
fffff800`0419736f 4c3bd7 cmp r10,rdi
fffff800`04197372 7445 je nt!ObpQueryNameString+0xc9 (fffff800`041973b9)
fffff800`04197374 483bdf cmp rbx,rdi
fffff800`04197377 7505 jne nt!ObpQueryNameString+0x8e (fffff800`0419737e)
fffff800`04197379 408af7 mov sil,dil
fffff800`0419737c eb06 jmp nt!ObpQueryNameString+0x94 (fffff800`04197384)
fffff800`0419737e 66397b08 cmp word ptr [rbx+8],di
fffff800`04197382 74f5 je nt!ObpQueryNameString+0x89 (fffff800`04197379)
fffff800`04197384 8a842420010000 mov al,byte ptr [rsp+120h]
fffff800`0419738b 88442428 mov byte ptr [rsp+28h],al
fffff800`0419738f 4c894c2420 mov qword ptr [rsp+20h],r9
fffff800`04197394 458bc8 mov r9d,r8d
fffff800`04197397 4c8bc2 mov r8,rdx
fffff800`0419739a 408ad6 mov dl,sil
fffff800`0419739d 41ffd2 call r10

        我们观察到其中一个比较特别的指令,该指令通过硬编码从内存中取出一个值放到了r10。

fffff800`0419734d 4d8b94c5807b2200 mov r10,qword ptr [r13+rax*8+227B80h]

        同时查看IDA 给出的反汇编后发现,IDA 解析处了该硬编码地址代表的含义为对象类型表。
这里写图片描述
        这样的话后面的指令就比较好理解了。如果对象对应的类型的ObpQueryNameString 不为NULL,调用它即可。
这里写图片描述

这里写图片描述

        我们来看一看Wrk 中给出的IopQueryNameInternal 函数实现。

NTSTATUS
IopQueryName(IN PVOID Object,IN BOOLEAN HasObjectName,OUT POBJECT_NAME_INFORMATION ObjectNameInfo,IN ULONG Length,OUT PULONG ReturnLength,IN KPROCESSOR_MODE Mode)/*++函数描述:This function implements the query name procedure for the Object Managerfor querying the names of file objects.此函数为对象管理器中文件对象的解析名称函数。
Arguments:Object - Pointer to the file object whose name is to be retrieved.HasObjectName - Indicates whether or not the object has a name.ObjectNameInfo - Buffer in which to return the name.Length - Specifies the length of the output buffer, in bytes.ReturnLength - Specifies the number of bytes actually returned in theoutput buffer.Mode = Processor mode of the callerReturn Value:The function return value is the final status of the query operation.--*/{UNREFERENCED_PARAMETER (Mode);return IopQueryNameInternal( Object,HasObjectName,FALSE,//第三个参数传FALSEObjectNameInfo,Length,ReturnLength,Mode );
}NTSTATUS
IopQueryNameInternal(IN PVOID Object,IN BOOLEAN HasObjectName,IN BOOLEAN UseDosDeviceName,OUT POBJECT_NAME_INFORMATION ObjectNameInfo,IN ULONG Length,OUT PULONG ReturnLength,IN KPROCESSOR_MODE Mode)/*++UseDosDeviceName - 是否将文件对象的设备对象部分转换为dosdevice 形式的名称空间或者常规的\device 名称空间
--*/{// ...// 我们当前的函数调用UseDosDeviceName 为FALSEif (UseDosDeviceName) {// ...} else {status = ObQueryNameString( (PVOID) fileObject->DeviceObject,deviceNameInfo,Length,&lengthNeeded );}if (!NT_SUCCESS( status )) {if (status != STATUS_INFO_LENGTH_MISMATCH) {return status;}}p = (PWSTR) (ObjectNameInfo + 1);try {if (UseDosDeviceName && dosLookupSuccess) {// 当前UseDosDeviceName 为FALSE} else {RtlCopyMemory( ObjectNameInfo,deviceNameInfo,lengthNeeded > Length ? Length : lengthNeeded );}ObjectNameInfo->Name.Buffer = p;p = (PWSTR) ((PCHAR) p + deviceNameInfo->Name.Length);deviceNameOverflow = FALSE;if (lengthNeeded > Length) {*ReturnLength = lengthNeeded;deviceNameOverflow = TRUE;}// ...if (((Mode == UserMode) && (!UseDosDeviceName)) ||!(fileObject->Flags & FO_SYNCHRONOUS_IO)) {//// 如果从Ring3 调用的话,是符合((Mode == UserMode) && (!UseDosDeviceName)) 条件的// 如果不是同步I/O 操作的话,同样也要走到这里// Query the name of the file based using an intermediary buffer.//status = IopQueryXxxInformation( fileObject,FileNameInformation,length,Mode,(PVOID) fileNameInfo,&lengthNeeded,TRUE );} else {//// 如果是内核请求,而且文件是同步I/O 操作的话,需要一种不需要获得文件锁就获取文件文件名的方法。如果需要获得文件锁的话,可能导致死锁。因为文件锁可能已经被获得了。//status = IopGetFileInformation( fileObject,length,FileNameInformation,fileNameInfo,&lengthNeeded );}}finally {//// Finally, free the temporary buffer.//ExFreePool( buffer );}return status;
}

        下面我们简单验证上面的思路,然后最后查看Wrk 给出的两种获得文件名称的方法。
这里写图片描述

        下面我们查看函数IopQueryXxxInformation 的实现。

ObReferenceObject( FileObject );//// 检查文件是否被同步打开,如果是的话,等待直到当前线程拥有该文件// 如果这个文件打开时指定的操作不是同步操作的话,初始化一个本地的事件。//if (FileObject->Flags & FO_SYNCHRONOUS_IO) {BOOLEAN interrupted;if (!IopAcquireFastLock( FileObject )) {status = IopAcquireFileObjectLock( FileObject,Mode,(BOOLEAN) ((FileObject->Flags & FO_ALERTABLE_IO) != 0),&interrupted );if (interrupted) {ObDereferenceObject( FileObject );return status;}}KeClearEvent( &FileObject->Event );synchronousIo = TRUE;} else {KeInitializeEvent( &event, SynchronizationEvent, FALSE );synchronousIo = FALSE;}

这里写图片描述

         现在查看另一个函数的操作。它为什么能够不获得锁而的到对象名称?

NTSTATUS
IopGetFileInformation(IN PFILE_OBJECT FileObject,IN ULONG Length,IN FILE_INFORMATION_CLASS FileInformationClass,OUT PVOID FileInformation,OUT PULONG ReturnedLength)/*++Routine Description:内核模式,通过对象管理器,想以异步方式获得同步打开的文件对象的信息的时候调用此函数。--*/{PIRP irp;NTSTATUS status;PDEVICE_OBJECT deviceObject;KEVENT event;PIO_STACK_LOCATION irpSp;IO_STATUS_BLOCK localIoStatus;PAGED_CODE();//// 操作之前引用对象,防止其被删除//ObReferenceObject( FileObject );//// 同步事件,通知我们的驱动,操作已经完成。//KeInitializeEvent( &event, SynchronizationEvent, FALSE );//// 得到文件对应的设备对象//deviceObject = IoGetRelatedDeviceObject( FileObject );//// 盛情并初始化一个IRP//irp = IoAllocateIrp( deviceObject->StackSize, FALSE );if (!irp) {// 出错的话直接解引用文件对象并退出。ObDereferenceObject( FileObject );return STATUS_INSUFFICIENT_RESOURCES;}irp->Tail.Overlay.OriginalFileObject = FileObject;irp->Tail.Overlay.Thread = PsGetCurrentThread();irp->RequestorMode = KernelMode;//// 在IRP 中设置服务无关的参数。在irp 中设置特定的query name 标志可以确保将不会执行标准的同步文件的完成操作// 因为这个标志告诉I/O 完成不要这么做。// 这也就是为什么这个函数对于同步文件的操作与众不同// 设置异步APC 函数为NULL。irp->UserEvent = &event;irp->Flags = IRP_SYNCHRONOUS_API | IRP_OB_QUERY_NAME;irp->UserIosb = &localIoStatus;irp->Overlay.AsynchronousParameters.UserApcRoutine = (PIO_APC_ROUTINE) NULL;//// 设置主功能码。//irpSp = IoGetNextIrpStackLocation( irp );irpSp->MajorFunction = IRP_MJ_QUERY_INFORMATION;irpSp->FileObject = FileObject;//// 交互方式为缓冲区I/O //irp->AssociatedIrp.SystemBuffer = FileInformation;irp->Flags |= IRP_BUFFERED_IO;//// Copy the caller's parameters to the service-specific portion of the// IRP.//irpSp->Parameters.QueryFile.Length = Length;irpSp->Parameters.QueryFile.FileInformationClass = FileInformationClass;//// 将IRP 插入到线程的IRP 列表的头部。//IopQueueThreadIrp( irp );//// 调用底层驱动//status = IoCallDriver( deviceObject, irp );//// 等待底层的驱动执行完毕后设置事件。//if (status == STATUS_PENDING) {(VOID) KeWaitForSingleObject( &event,Executive,KernelMode,FALSE,(PLARGE_INTEGER) NULL );status = localIoStatus.Status;}*ReturnedLength = (ULONG) localIoStatus.Information;return status;
}

         通过上面的代码我们可以发现,两个函数的实现的区别就是一个增加了同步的操作,一个没有,而且IopGetFileInformation 函数在irp 中设置IRP_OB_QUERY_NAME标志,这个标志告诉I/O 完成不要执行通常的同步文件操作完成时执行的操作,这也就是为什么这个函数对于同步文件的操作与众不同。
         通过上面的研究我们发现了,当Ring3 对于同步文件执行NtQueryObject 以获得文件名的时候,将导致线程死锁。而Ring0 获取文件名是不会导致线程死锁的,无论是同步文件还是异步文件。


推荐阅读
author-avatar
陈艹桂肀琰
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有