引用注明>> 【作者:张佩】【原文:www.YiiYee.cn/blog】
今天开电脑的时候,刚完成用户登陆,就遇到一个蓝屏。桌面还没有进去呢。趁着系统正处于抓取dump文件的过程中,赶紧拍了一张照,留作纪念。造成蓝屏的不是别人,乃是负责图形渲染和显示的显卡驱动:Nvidia显卡驱动。
我使用的系统:Win Blue x64。
事出有因
显卡驱动负责桌面系统的渲染和显示,其重要性不是一点点。所以轻易是不可能蓝屏的。我刚开始也有点纳闷,想自己昨天究竟做了什么,使得自己一大早的就遇到个天雷滚滚从天降——开机蓝屏。后来看了系统显示的错误原因,才明白过来。从上图中看到,这是在Verifier开启的前提下,诱发出来的NV显卡驱动的癫痫病。
Verifier为啥会开启呢?我想起来了,这确实事出有因。可追溯到我昨天看的一篇介绍数字签名的文章,它介绍了sigverif.exe这个工具,可以检测系统中已安装而未被数字签名的驱动程序。在运行了这个工具后,我很欣喜地发现系统中所有驱动程序都是签过名的。
其实这个结果解释了我的一个疑惑。因为我记得自己前段时间在运行Verifier的时候,它总是能找到两个未签名的驱动程序。其中一个是我安装的VClone虚拟光驱软件附带的内核驱动程序。从VClone的官方文档来看,它是有数字签名的,设备管理器中也有正确的显示。为什么Verifier把它列为嫌犯,我对此一直都很疑惑。
现在有了sigverif作为对照,我又再次运行了Verifier。选择验证未签名的驱动程序,果然还是有两个被列了出来。如下图所示。
就这个情况,我研究了半天,没有一个结论。但过程中,我出现了一个小小的操作失误。在选择驱动程序进行验证的时候,我选择了一个不可逆的验证:自动选择这台计算机上安装的所有驱动程序。
我选择这一项的初衷,是要看看verifier检索到的驱动列表,和Sigverif检索的驱动列表的区别。不料这个过程竟然是不可逆的,即使我退出后,再次选择“删除现有设置”,也已经没有用。
但当时,我却没这么觉得。我以为通过一些动作,已经把Verifier设置都清空了。其实却不然呢。这正是发生今天这个问题的初始缘由了。
自我救赎
重启后,我又试了两次,希冀可以登录到桌面后快速地关掉Verifier。但事实却很无情,我又多遇到了两次迅捷无比的蓝屏。所以我就进入到安全模式。Windows在安全模式下不使用IHV的显示驱动,而是加载微软自己的display only显示驱动。
这一次我是安全的。安全模式救了我。我运行verifier,并在此选择“删除现有设置”项。在提示重启出现的时候,我服从并重启。重启到正常的系统,这次已无问题了。
调试分析
活过来后,我第一个启动的是Windbg,并加载dump文件。错误类型DRIVER_VERIFIER_DETECTED_VIOLATION对应的BSOD号是0xC4,自动分析结果如下:
2: kd> !analyze -v ******************************************************************** * * Bugcheck Analysis * * * ******************************************************************* DRIVER_VERIFIER_DETECTED_VIOLATION (c4) A device driver attempting to corrupt the system has been caught. This is because the driver was specified in the registry as being suspect (by the administrator) and the kernel has enabled substantial checking of this driver. If the driver attempts to corrupt the system, bugchecks 0xC4, 0xC1 and 0xA will be among the most commonly seen crashes. Arguments: Arg1: 00000000000000f6, Referencing user handle as KernelMode. Arg2: 0000000000000100, Handle value being referenced. Arg3: ffffe00008f53900, Address of the current process. Arg4: fffff800028fc879, Address inside the driver that is performing the incorrect reference. Debugging Details: ------------------
自动分析的结果非常重要。它的第一个参数指明了Verifier错误类型,0xf6表示驱动程序在引用一个用户句柄的时候,把它的类型错误地指示为KernelMode。打开Windbg的帮助文档,看到更详细的参数解释:
Parameter 1 | Parameter 2 | Parameter 3 | Parameter 4 | Cause of Error |
0xF6 (Windows 7 operating systems and later) | Handle value being referenced | Address of the current process | Address inside the driver that performs the incorrect reference | A driver references a user-mode handle as kernel mode. |
从上面得到另一个很重要的信息:这个错误类型,只在Win7以后的系统上才存在。
它的第三个参数是被应用的句柄,值为0x100。它很明显是一个用户层句柄,因为Winidows系统上的内核句柄,其高位是被置1的。比如32位系统上,内核句柄应该是0x80xxxxxx,64位系统上是0xffffffff’80xxxxxx。虽然没有明确的文档说明这一点,但仅仅根据我们的观察,可以从经验上证明之。
所以自动分析是言之有物的,它是在说:在一个地址为ffffe00008f53900(参数3)的用户进程环境中, NV显卡驱动在代码执行到地址fffff800028fc879(参数4)附近时,以kernelMode的方式使用了一个用户句柄0x100。
2: kd> !handle 0x100 PROCESS ffffe00008f53900 SessionId: 1 Cid: 1538 Peb: 7ff725f06000 ParentCid: 0c20 DirBase: 17bda2000 ObjectTable: ffffc00003321400 HandleCount: Image: rundll32.exe Handle Error reading handle count. 0100: Object: ffffc000056b72a0 GrantedAccess: 00020019 (Protected) (Inherit) (Audit) Entry: ffffc000033b0400 Object: ffffc000056b72a0 Type: (ffffe00000119730) Key ObjectHeader: ffffc000056b7270 (new version) HandleCount: 1 PointerCount: 32768 Directory Object: 00000000 Name: \REGISTRY\MACHINE\SYSTEM\CONTROLSET001\CONTROL\CLASS\{4D36E968-E325-11CE-BFC1-08002BE10318}\0000\NVSPCAPS 2: kd> !process ffffe00008f53900 PROCESS ffffe00008f53900 SessionId: 1 Cid: 1538 Peb: 7ff725f06000 ParentCid: 0c20 DirBase: 17bda2000 ObjectTable: ffffc00003321400 HandleCount: Image: rundll32.exe VadRoot ffffe00008d27330 Vads 61 Clone 0 Private 515. Modified 15118. Locked 0. DeviceMap ffffc000039412a0 Token ffffc00003359060 ElapsedTime 00:00:01.055 UserTime 00:00:00.000 KernelTime 00:00:00.000 QuotaPoolUsage[PagedPool] 150496 QuotaPoolUsage[NonPagedPool] 7792 Working Set Sizes (now,min,max) (1608, 50, 345) (6432KB, 200KB, 1380KB) PeakWorkingSetSize 1608 VirtualSize 74 Mb PeakVirtualSize 74 Mb PageFaultCount 1630 MemoryPriority BACKGROUND BasePriority 8 CommitCharge 620 THREAD ffffe00008ec8080 Cid 1538.153c Teb: 00007ff725f0e000 Win32Thread: fffff901469e8b70 RUNNING on processor 2 Not impersonating DeviceMap ffffc000039412a0 Owning Process ffffe00008f53900 Image: rundll32.exe Attached Process N/A Image: N/A Wait Start TickCount 8751 Ticks: 50 (0:00:00:00.781) Context Switch Count 593 IdealProcessor: 2 UserTime 00:00:00.000 KernelTime 00:00:00.140 Win32 Start Address 0x00007ff725f33f0c Stack Init ffffd000235d7c90 Current ffffd000235d6f90 Base ffffd000235d8000 Limit ffffd000235d2000 Call 0 Priority 8 BasePriority 8 UnusualBoost 0 ForegroundBoost 0 IoPriority 2 PagePriority 5 Child-SP RetAddr Call Site ffffd000`235d6658 fffff800`d7eea6a8 nt!KeBugCheckEx ffffd000`235d6660 fffff800`d7eeff98 nt!VerifierBugCheckIfAppropriate+0x3c ffffd000`235d66a0 fffff800`d7db7e73 nt!VfCheckUserHandle+0x1b8 ffffd000`235d6780 fffff800`d7c285d5 nt! ?? ::NNGAKEGL::`string'+0x10503 ffffd000`235d6820 fffff800`d7c402d6 nt!ObReferenceObjectByHandle+0x25 ffffd000`235d6870 fffff800`d79d74b3 nt!NtQueryValueKey+0x136 ffffd000`235d6b20 fffff800`d79cf900 nt!KiSystemServiceCopyEnd+0x13 (TrapFrame @ ffffd000`235d6b90) ffffd000`235d6d28 fffff800`028fc879 nt!KiServiceLinkage ffffd000`235d6d30 fffff800`028fc0c2 nvlddmkm+0x9a879 ffffd000`235d6de0 fffff800`02946cbe nvlddmkm+0x9a0c2 ffffd000`235d6e80 fffff800`029151d4 nvlddmkm+0xe4cbe ffffd000`235d6f00 fffff800`02907e5c nvlddmkm+0xb31d4 ffffd000`235d6f50 fffff800`0317c1c3 nvlddmkm+0xa5e5c ffffd000`235d7360 fffff800`0290730b nvlddmkm!nvDumpConfig+0x29fdeb ffffd000`235d73a0 fffff800`0315e9b9 nvlddmkm+0xa530b ffffd000`235d74c0 fffff800`031fcc05 nvlddmkm!nvDumpConfig+0x2825e1 ffffd000`235d7590 fffff800`02301e5c nvlddmkm!nvDumpConfig+0x32082d ffffd000`235d75c0 fffff800`022c8e03 dxgkrnl!DXGADAPTER::DdiEscape+0x48 ffffd000`235d75f0 fffff960`001813a3 dxgkrnl!DxgkEscape+0x573 ffffd000`235d7ab0 fffff800`d79d74b3 win32k!NtGdiDdDDIEscape+0x53 ffffd000`235d7b00 00007ff8`349d14aa nt!KiSystemServiceCopyEnd+0x13 (TrapFrame @ ffffd000`235d7b00) 0000006b`439bf068 00000000`00000000 0x00007ff8`349d14aa
这个进程rundll32是一个执行dll调用的通用的宿主进程,所以它的父进程比较能说明问题。我期望它的父进程是NV相关的进程,但最后发现CID为0xc20的进程为桌面进程。可能的情况是桌面进程调用了D3D的相关功能,进入NV显卡驱动并爆发了问题。在进入内核驱动时,用户程序传入了一个句柄参数,这个句柄指向一个和NV显卡相关的注册表键而,内核不正确地使用了这个句柄并导致问题。这个相关的注册表键值的路径为:\REGISTRY\MACHINE\SYSTEM\CONTROLSET001\CONTROL\CLASS\{4D36E968-E325-11CE-BFC1-08002BE10318}\0000\NVSPCAPS
2: kd> !process 0 0
*省略其它进程信息*
PROCESS ffffe00008810900
SessionId: 1 Cid: 0c20 Peb: 7ff7be8a6000 ParentCid: 0c10
DirBase: 15a40e000 ObjectTable: ffffc000041a5440 HandleCount:
Image: explorer.exe
逆推代码错误
根据上面的分析内容,已能很轻松地指症了。从它的调用栈上可以看出来,在进入Verifier检测函数前,系统调用的函数是ObReferenceObjectByHandle。我们看这个函数的声明:
NTSTATUS ObReferenceObjectByHandle( _In_ HANDLE Handle, _In_ ACCESS_MASK DesiredAccess, _In_opt_ POBJECT_TYPE ObjectType, _In_ KPROCESSOR_MODE AccessMode, _Out_ PVOID *Object, _Out_opt_ POBJECT_HANDLE_INFORMATION HandleInformation );
关于AccessMode,MSDN上的解释是:
AccessMode [in]
Specifies the access mode to use for the access check. It must be either UserMode or KernelMode. Drivers should always specify UserMode for handles they receive from user address space.
所以,对于0x100的用户句柄,如果在调用ObReferenceObjectByHandle的时候,指示的Accessmode为KernelMode,就会在Verifier检验函数中产生一个类型为0xC4/0xF6的BSOD。这也是一个比较合乎情理的错误原因。
到这里问题到还没有结束,因为ObReferenceObjectByHandle是被间接调用的,NV驱动直接调用的函数是ZwQueryValueKey(它没有AccessMode这个参数)。为什么是ZwQueryValueKey呢?这涉及到Zwxxx和Ntxxx两组系统API的区别。见下面这一段stack。
ffffd000`235d6870 fffff800`d79d74b3 nt!NtQueryValueKey+0x136 ffffd000`235d6b20 fffff800`d79cf900 nt!KiSystemServiceCopyEnd+0x13 ffffd000`235d6d28 fffff800`028fc879 nt!KiServiceLinkage ffffd000`235d6d30 fffff800`028fc0c2 nvlddmkm+0x9a879
在内核中调用Zwxxx函数,它会经过一系列复杂过程,最终调用到对应的Ntxxx函数。重要的一点是,调用Zwxxx函数会把当前线程的Previous Mode设置成Kernel Mode(参考文章:OSR)。
一个在内核中执行的线程,它既可能是从用户程序下来的,也可能是一个一直在内核中运行的系统线程。为了区分这种情况,线程结构体中保存了一个变量,保存线程此前的Mode(Previous Mode)。对于一个从用户层调下来的线程,它的Previous Mode是User Mode。但如果它调用了哪怕一次Zwxxx函数,其Previous Mode将被改成Kernel Mode,好像它再一次陷入了内核(从内核陷入内核)。
在这个例子中,对ZwQueryValueKey的调用,很可能影响到接下来NtQueryValueKey中调用ObReferenceObjectByHandle时的输入参数。所以,在驱动程序中调用Native API,使用Ntxxx函数比Zwxxx函数更稳妥。
这些内容比较隐晦,涉及很多未文档内容。我不确定。但我怀疑:如果NV驱动把调用ZwQueryValueKey的代码改成直接调用NTQueryValueKey,可能就会解决问题。
其它
在Windbg分析完之后,我看了一下我当前使用的NV驱动版本是331.82,日期为2013年11月,大约两个月前更新的,也算是比较新。我立刻到NV的官方网站上查看和我显卡匹配的最新驱动(GTX 670M),有2014年1月份的最新WHQL版本:332.21。我见此便立刻下载了。我还是有点小胆怯的,所以没再去帮助NV验证最新的驱动是否已经解决了这个问题。如果有NV的Driver工程师看到我这篇文章,可以试一试。我保留了dump文件,需要时也可以向我索取。