如何使用 Microsoft 的 Driver Verifier 来解释无法分析的崩溃转储文件






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