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

之前遗留的一个问题

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,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 <PERF> (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 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 caller

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

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

这里写图片描述

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

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

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值