这篇文章解释了如何使用驱动程序验证工具来分析崩溃转储文件。
使用Microsoft驱动程序验证工具
如果您曾经使用Windows的调试工具来分析崩溃转储,那么毫无疑问,您已经使用WinDbg打开了一个崩溃转储文件。WinDbg将对崩溃文件执行内部分析,并建议您从!analyze命令开始。该命令输出堆栈以及许多其他信息。执行此操作时,堆栈底部将显示转换为内核模式的线程,然后从那里开始,您向上遍历堆栈以查看是否存在罪魁祸首驱动程序。虽然这是一种可靠的调试技术,但有时崩溃转储或其中的一组将无法分析。内存中没有任何模式可以指出导致系统崩溃的原因,或者内存可能已损坏,因为崩溃转储文件实际上指向Ntsokrnl.exe或win32k.sys。
有一种方法可以通过使用Microsoft驱动程序验证工具将无法处理的崩溃转换为可分析的崩溃。此工具随Windows的每个版本一起提供,不是单独安装的。它不可见,因此不在“开始”菜单或“控制面板”中的“管理工具”中。通过在“开始”菜单的“运行”框中键入“verifier”来启动它,但是为了使用它,您应该知道它是如何工作的。本文将介绍如何使用此工具将无法分析的崩溃转换为可分析的崩溃。驱动程序验证工具包含许多选项,其中一些应该严格避免。
要启动驱动程序验证程序,请在“开始”菜单的“运行”框中键入“Verifier.exe”。第一个数据框显示列表选项。选择的选项是“创建自定义设置(用于代码开发人员)”。避免使用默认的“标准设置”选项。单击“下一步”后,我们选择“从完整列表中选择各个设置”。注意,我们没有选择任何默认设置。单击Next之后,我们会看到一个选项列表,范围从“特殊池”到“杂项检查”。实际上,除了“低资源模拟”之外,我们都选择了它们。低资源模拟正是这样一种设置,因此我们不希望重新启动并让任何设备驱动程序实际测试其行为,因为它的资源是模拟的,并且正在被有目的地耗尽。“特殊池”选项将在本文后面讨论。让我们首先检查“强制IRQL检查”。
假设一个驱动程序接触到一段分页内存。IRQL当前处于被动级别,这是所有用户模式代码运行的IRQL。但是,由于驱动程序已经接触到一块内存,内存管理器必须将该数据引入物理RAM并将其连接到该分页虚拟地址。现在,驱动程序执行一个操作,使IRQL上升到DISPATCH_LEVEL,并立即再次引用同一个分页缓冲区。要想让它显示为被操作系统捕获的bug,内存管理器需要在第一次引用、IRQL提升和第二次引用之间的少量指令发生的很短时间内,决定是否需要重用(或发送到分页文件)。这是极不可能的。“强制IRQL检查”所做的是,在启用该选项验证驱动程序时,它会导致IRQL移动到分派级别或更高级别。内存管理器获取连接到分页池虚拟内存的系统工作集的所有页,并断开这些页与物理内存的连接。
请注意,工作集是分配给进程的物理内存量,该量由内存管理器确定,因为操作系统已根据内存需求和分页速率监视该进程的行为。所以现在,如果这个驱动程序在DISPATCH_LEVEL再次访问这个缓冲区,它将生成一个页面错误,因为内存管理器将不得不去修复那个虚拟内存和那个物理内存之间的连接。此时内存管理器将检查当前的IRQL。它将看到它是DISPATCH_LEVEL或更高级别,然后确定这是一个非法操作,从而导致系统崩溃。这就是你想要的。您希望找到哪个驱动程序有足够的错误,以至于在执行某些非法操作时被抓到,这些操作将使系统在某些最终用户的计算机上或在某些不受控制的环境中崩溃。强制IRQL检查将揭示哪种驱动程序确实存在这些类型的错误,并因此暴露罪魁祸首驱动程序。
池跟踪选项对于驱动程序内存泄漏非常有用。I/O验证和增强的I/O验证使操作系统驱动程序验证程序代码对驱动程序传递到的和驱动程序传递回系统的数据结构执行一些严格的检查。这种数据结构称为中断请求包(IRP)。IRP的结构有一些特殊的规则。它必须指向有效的结构。它必须有一组一致的值。因此,驱动验证器将在驱动程序对其进行操作后检查该包,以确保其仍处于一致状态。
要创建一组步骤来将无法分析的崩溃转换为可分析的崩溃,我们必须认识到某些崩溃与某些条件相关,这些条件由这些选项描述。因此,在这些选项后单击Next,选择“从列表中选择驱动程序”。不要选择“自动选择系统上安装的所有驱动程序”。此时,菜单将加载驱动程序列表。在该列表下面是一个按钮,说明“将当前加载的驱动程序添加到列表中”。也许,你知道有一个驱动程序是有问题的,你可能想添加。拖放Provider部分以隔离那些不是来自Microsoft的驱动程序,并快速清点。在选择这些可疑的驱动程序、对这些驱动程序启用验证器并重新启动后,查看系统是否崩溃。如果系统没有崩溃,则采取另一个步骤,选择所有未签名的驱动程序和/或第三方设备驱动程序,并在这些驱动程序上运行验证程序。如果系统没有崩溃,那么作为最后手段,对每个驱动程序运行驱动程序验证程序。但是,不要一蹴而就。一次选择大约10或20个驱动程序,启用验证程序,然后重新启动。如果选择所有配置了这些选项的驱动程序,则系统可能需要20分钟才能重新启动。系统的行为可能在短时间内看起来有所不同,但最终会进入稳定状态(如果您将此作为练习,而不是尝试将无法分析的崩溃转化为可分析的崩溃)。
使用Notmyfault.exe测试驱动程序
目前受雇于微软的Mark Russonivich编写了一个名为“Notmyfault.exe”的测试驱动程序。此实用程序包含一个设备驱动程序myfault.sys,它将导致符合特定操作系统条件的特定类型的崩溃。在他编写的其他工具中,这个工具尤其是对于使用和理解系统崩溃以及如何避免它们是非常宝贵的。虽然不是必需的,但此工具最好在虚拟环境中运行。虚拟环境是作为计算环境运行的软件层。如果您下载了一个VMWare Workstation的试用版并安装了它,您将能够在正在运行的操作系统内(但与该操作系统分离)安装一个操作系统。试试看,安装一个旧版本的XP。
即使您不关心性能,但如果您的目标是所有驱动程序,那么还有另一个原因要追求成批的驱动程序,这就是特殊池选项。当我们调用NotmyFault.exe程序将控制请求发送到myfault.sys驱动程序以执行缓冲区溢出时,myfault.sys驱动程序将从内核内存分配一个缓冲区,然后写入缓冲区数组的末尾。这将损坏内存,如图所示:
注意,我们检查了缓冲区溢出选项。当我们按下“DoBug”按钮时,一些随机缓冲区将在内核内存中被覆盖。因此,被覆盖的内存已损坏。但是,仅仅损坏内存可能会导致系统崩溃,也可能不会,直到引用了损坏内存的内容。然后,系统就会崩溃。因此,在内存损坏和检测到损坏之间可能有很长的延迟。通常,另一个驱动程序或内核进行引用。MyFault.sys分配一个非分页的池缓冲区,并在末尾写入一个字符串,从而损坏池头和后面的数据结构。所以,我们按一次“Do bug”,什么也没发生。也许,按那个按钮十次,还是什么都没发生。不过,有一点是肯定的,我们现在有一个非常病态的内核内存。如果仍然没有崩溃,那么运行类似于Internet Explorer的程序,看看它是否引用了足以检测内存损坏的操作系统。如果这不起作用,那么运行一些更重的程序,更有可能导致系统崩溃,比如WindowsMessenger。
假设系统现在崩溃了。但当系统崩溃时,是不是Windows Messenger导致了系统崩溃?当然没有。但是,在一些内核模式的软件中发生了一些事情(不是Windows Messenger),这些事情被间接调用,导致系统崩溃。当内核检测到损坏的池时,蓝屏显示启用驱动程序验证程序。它告诉你发生了什么,但不是为什么。此时,我们将检查崩溃文件。崩溃文件显示了堆栈,但随后在跟踪堆栈上显示了一个Microsoft设备驱动程序,这可能是一个非常重要的驱动程序。但是,驱动程序只引用了损坏的内存。它实际上并没有损坏内存,这就是系统崩溃的原因。
现在,我们执行相同的测试,但是启用了驱动程序验证程序,启用了所有选项(特别是特殊池,但再次声明,不启用低资源模拟)。当您按下“DoBug”按钮时,驱动程序将尝试写入到其分配的末尾,但会在实际操作中被捕获。系统立即崩溃,但更重要的是,它直接指向myfault.sys。也就是说,我们当场抓到一名司机。触发的验证选项是特殊池选项。当使用特殊池验证选项集验证驱动程序时,Windows会尝试从内存的特殊区域满足对它的内存分配,因此命名为特殊池。此区域是特殊的,因为此区域中的每个其他页都是无效的内存页,因为它将驱动程序缓冲区与分配缓冲区的内存顶部对齐。所以,当司机从缓冲区的末端溜走时,它不会坐在其他缓冲区中,但最终会碰到这些无效的内存页中的一页。而且,仅仅是它接触到一个无效的内存页就触发了一个页面错误。页面错误处理程序查看引用的内容,发现从内核模式访问的内存页无效,并将立即使系统崩溃并告诉您驱动程序有问题:
但是,有两个条件,即使您在特定驱动程序或系统范围内启用了特殊池。发送到特殊池的分配必须略少于一页。也就是说,在x86系统上,分配的页大小必须小于4kb或4096字节。所以,当一个驱动程序进行大的内存分配时,它不会来自特殊的池。这意味着,如果它覆盖了缓冲区,它就没有myfault.sys演示中的保护和检查。特殊池是不需要重新启动的驱动程序验证选项之一,但请记住它是一个有限的资源。因此,当特殊池用完时,正在验证的驱动程序将把它们的池分配发送到普通池。换言之,它们将在没有上述保护的情况下进行验证。
另一个有用的测试是系统代码覆盖测试。当驱动程序中有一个错误损坏指针,并且指针指向操作系统内核或其他引导启动驱动程序的代码时,系统代码覆盖就会发生。大多数情况下,这种访问不会被检测到。在这些情况下,当它被检测到时,Windows将识别出一个驱动程序正在试图覆盖操作系统或另一个驱动程序的部分代码。要做到这一点,Windows必须有一个称为系统代码写入保护的工具。系统代码写入保护是内存管理器将操作系统和驱动程序的代码页标记为只读的机制。因此,如果驱动程序试图写入这些页面,则会触发页面错误,内存管理器会用停止代码来指示驱动程序试图修改代码,从而使系统崩溃。但是,由于性能原因,系统代码写入功能被关闭。也就是说,为了提高性能,内核在大多数系统上没有标记为只读。Windows为了节省CPU上作为缓存的translation look aside缓冲区的空间,将虚拟地址映射到物理地址,将操作系统代码和引导启动驱动程序映射到一个大的内存页中。在典型的x86机器上,标准或“小”页是4KB。但是,像驱动程序或内核这样的映像是在4kb的段中定义的,其中代码和数据在映像中。如果操作系统将包含代码页和数据页的整个映像加载到一个4 MB的大页中,则它别无选择,只能将该页上的内存保护设置为读/写。否则,内核和驱动程序将无法修改它们自己的数据(也映射到那个大页面)。因此,系统几乎总是设置为关闭系统代码覆盖。
简短的说明
一个错误的想法是,如果像记事本这样的可执行文件被启动,整个image就会被加载。实际上,只加载其中的一部分,这被称为“惰性分配器”。随着更多功能的使用,这个可执行文件的更多image将从磁盘中读取。必要的dll也不会全部加载。只加载这些dll的引用部分。这称为“虚拟分配”。Windows中任何可共享的内存都是共享的。这意味着代码和dll,但不是数据。一种误解是,如果加载两个记事本实例,则会有两个加载的记事本image。Windows意识到有一个映像的第二个实例已经将它的一部分加载到物理RAM中,并自动将两个虚拟映像连接到相同的底层页面。但是,在每个加载的记事本实例中键入的数据是对应实例的私有数据。因此,数据不是共享的,但是执行记事本的代码是共享的,dll也是共享的。
有一种方法可以在注册表中显式启用系统代码覆盖,但这不是必需的。当验证器打开时,即使使用最小的设置,也会启用系统代码覆盖。因此,当您打开verifier时,内核将用小页面映射自身和驱动程序,因此尝试写入代码将立即生成蓝屏。如果在Notmyfault.exe上选择“代码覆盖”单选按钮,myfault.sys将覆盖NtReadFile的前几个字节,NtReadFile是一个常用的系统函数。NtReadFile是代表从任何文件句柄读取的线程调用的底层系统调用。
因此,当我们按下“Do bug”时,代码将被覆盖(因为这是允许的,因为我们运行的是一个默认系统,它的内核和引导驱动程序映像映射到一个标记为读/写的大页面)。当代码被重写时,它将很容易被检测到,因为其他东西将调用NtReadFile函数,该函数将运行到某些被重写的指令中,从而导致崩溃。如果你看一下停止代码,就没有指向驱动程序的指针,因为导致这次崩溃的驱动程序早已不复存在。如果我们查看崩溃转储文件,我们可以很容易地找到一个关键系统组件驱动程序,如Win32k.sys,现在这是一个误诊。我们可以使用最高级的调试命令,并保留一个无法分析的崩溃文件。同样,解决方案是使用驱动程序验证程序。在驱动程序验证程序打开的情况下按“Do bug”表示系统代码覆盖已打开。当它崩溃时,蓝屏立即指向myfault.sys,并且进一步的文本说明试图写入只读内存。崩溃转储文件以前指向与崩溃无关的驱动程序,现在指向正确的驱动程
粗略地说,当Windows崩溃转储文件从调试器的分析引擎中不正确时,目标是将这些文件转换为可分析的文件。驱动验证器是帮助实现这一点的工具,以及提高系统性能的工具。