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

来源:互联网 发布:网络女主播谈恋爱 编辑:程序博客网 时间:2024/05/22 21:39

之前遗留的一个问题

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 DBG            KIRQL SaveIrql;#endif            ObpBeginTypeSpecificCallOut( 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,rspfffff800`041972f3 4c894820        mov     qword ptr [rax+20h],r9fffff800`041972f7 44894018        mov     dword ptr [rax+18h],r8dfffff800`041972fb 48895010        mov     qword ptr [rax+10h],rdxfffff800`041972ff 48894808        mov     qword ptr [rax+8],rcxfffff800`04197303 53              push    rbxfffff800`04197304 56              push    rsifffff800`04197305 57              push    rdifffff800`04197306 4154            push    r12fffff800`04197308 4155            push    r13fffff800`0419730a 4156            push    r14fffff800`0419730c 4157            push    r15fffff800`0419730e 4881ecc0000000  sub     rsp,0C0hfffff800`04197315 4c8bf2          mov     r14,rdxfffff800`04197318 c7442440010000c0 mov     dword ptr [rsp+40h],0C0000001hfffff800`04197320 33ff            xor     edi,edifffff800`04197322 897c2450        mov     dword ptr [rsp+50h],edifffff800`04197326 48897c2448      mov     qword ptr [rsp+48h],rdifffff800`0419732b 8d7701          lea     esi,[rdi+1]fffff800`0419732e 4088742431      mov     byte ptr [rsp+31h],silfffff800`04197333 40887c2430      mov     byte ptr [rsp+30h],dilfffff800`04197338 4c8d79d0        lea     r15,[rcx-30h]fffff800`0419733c 4c897c2470      mov     qword ptr [rsp+70h],r15fffff800`04197341 410fb64718      movzx   eax,byte ptr [r15+18h]fffff800`04197346 4c8d2db33ccbff  lea     r13,[nt!KiSelectNextThread <PERF> (nt+0x0) (fffff800`03e4b000)]fffff800`0419734d 4d8b94c5807b2200 mov     r10,qword ptr [r13+rax*8+227B80h]fffff800`04197355 41f6471a02      test    byte ptr [r15+1Ah],2fffff800`0419735a 0f853b090000    jne     nt!ObpQueryNameString+0x9ab (fffff800`04197c9b)fffff800`04197360 488bdf          mov     rbx,rdifffff800`04197363 48895c2468      mov     qword ptr [rsp+68h],rbxfffff800`04197368 4d8b92a0000000  mov     r10,qword ptr [r10+0A0h]fffff800`0419736f 4c3bd7          cmp     r10,rdifffff800`04197372 7445            je      nt!ObpQueryNameString+0xc9 (fffff800`041973b9)fffff800`04197374 483bdf          cmp     rbx,rdifffff800`04197377 7505            jne     nt!ObpQueryNameString+0x8e (fffff800`0419737e)fffff800`04197379 408af7          mov     sil,dilfffff800`0419737c eb06            jmp     nt!ObpQueryNameString+0x94 (fffff800`04197384)fffff800`0419737e 66397b08        cmp     word ptr [rbx+8],difffff800`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],alfffff800`0419738f 4c894c2420      mov     qword ptr [rsp+20h],r9fffff800`04197394 458bc8          mov     r9d,r8dfffff800`04197397 4c8bc2          mov     r8,rdxfffff800`0419739a 408ad6          mov     dl,silfffff800`0419739d 41ffd2          call    r10

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

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

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

这里写图片描述

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

NTSTATUSIopQueryName(    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 Manager    for 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 the        output 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,//第三个参数传FALSE                                 ObjectNameInfo,                                 Length,                                 ReturnLength,                                 Mode );}NTSTATUSIopQueryNameInternal(    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 为FALSE    if (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;    }

这里写图片描述

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

NTSTATUSIopGetFileInformation(    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 获取文件名是不会导致线程死锁的,无论是同步文件还是异步文件。

阅读全文
0 0