分析Windows的死亡蓝屏(BSOD)机制

发布者:Ox9A82
发布于:2016-06-06 21:47

  这篇文章本来是投Freebuf的,结果没过。就贴到博客里吧,图懒得发上来了  

  对于Windows系统来说,被人们视为洪水猛兽的蓝屏也是一种有利于系统稳定的机制。蓝屏其实是Windows系 统的一种自查机制,一但系统发现自己哪里有些不对劲后就立即抛出蓝屏,来阻止错误蔓延。倘若没有蓝屏机制,那么可能很小的一个错误最后会不断的酝酿导致系 统数据损坏的严重后果。而事实上因为Windows系统自身导致的蓝屏其实是少之又少的,更多的蓝屏诱因是各种驱动程序,因为作者个人对Rootkit类 程序感兴趣,因此在平时的学习过程中深感各种不良的内核HOOK或者过滤驱动是诱发蓝屏的小能手。当然不符合微软规定的编程方式或是软件BUG,比如常见 的IRQL错误和违反PatchGuard也会触发蓝屏。当我们理解了Windows蓝屏机制的重要意义之后,一个新的问题被提出来了,就是Windows蓝屏究竟是如何产生的呢?

 

首先我们可以做一个触发蓝屏的实验,用WindbgVMware虚 拟机进行双机调试,首先打开被调试虚拟机。虚拟机停留在启动菜单选项时选择以调试模式启动,其实这内核提供的一个功能,如果以这种配置启动 Windows,内核会通过串口向外寻找远端调试器,因为是虚拟机双机调试所以是启用的虚拟串口。所谓的启动菜单其实就是bootmgr程序,这个程序是 由MBR直接启动的。而且这可能是整个Windows系统中最奇葩的PE程序了,这个程序的一部分是实模式的指令一部分是保护模式的指令。bootmgr 会先执行实模式的部分,启动实模式的指令会把CPU状态转到保护模式,于是程序的保护模式指令开始启动,之后bootmgr会启动winload.exe 来进行系统内核的加载。 

Windbg成功挂载到内核后,内核会自动中断到Windbg调试器,可能很多人都只是输入G直接继续执行了。但是其实这里是很有搞头的,我们可以在Windbg中输入K指令来看一下栈回溯。

 

如图,此时内核其实是很初始的阶段,我们看到KiSystemStartup这个函数是内核初始化的主要函数,然后是初始化内核核心和内核执行体。如 果是只接触过linux内 核的朋友可能会有疑问,什么叫内核核心和内核执行体?其实这种划分来自于微软的定义。内核核心是内核中较低层的部分,实现基本的 功能。而内核执行体则是内核中较为上层的部分,我们常接触的就是这部分,各种管理器比如对象管理器、进程管理器也都在这部分。通过栈回溯我们看到中断时内 核处于刚刚初始化的阶段,而此时我们有一个绝佳的机会去跟踪内核的启动流程,如果有机会我会写一篇调试Windows内核初始化的文章。

我们回到正题,我们的目的是触发一次蓝屏然后跟踪蓝屏的产生流程。那么如何触发一次蓝屏呢?写一个驱动可以达到这个目的,但是太麻烦了,而且很多读者可能并没有接触过驱动开发。其实Windbg的一条命令就可以实现触发蓝屏,而且甚至MSDN都给出了方法

 

我们在Windbg中输入G,让虚拟机继续执行。等系统启动完毕后,用WindbgCtrl+Break抛出断点使系统中断到Windbg中。

 

Windbg调试器中输入.crash,系统就会触发蓝屏。如图

 

没错,这个就是当前最时尚潮流的蓝屏,与以前传统的蓝屏相比简直就是高富帅和屌丝的差别,但是其实无论是高富帅蓝屏还是屌丝蓝屏其实内部流程都是一样的,只是绘制出的图形不一样而已。

通过.crash命令触发的蓝屏会导致系统重启,我们是不能在调试器中获得通知的,这个时候就需要使用崩溃转储分析了。当你的Windows发生蓝屏崩溃后,系统会自动的储存一份转储文件在你的硬盘中,这份转储与我们通常调试程序时建立的dump文件是相似的,如图就是我用.crash命令触发蓝屏后 形成的转储文件。

 

注意转储文件的命名是以月日年-排号的顺序来命名的。我是在417写的这篇文章,而这是今天的第一个崩溃转储,所以命名就是041716-01Mini代表 迷你转储。转储文件其实就是崩溃发生时内存状态的一个备份,系统把它封装成一定的格式然后保存起来。Window提供了三种不同的类型的转储,其中Mini转储的体积是最小的,当然内容也是最少的。Mini转储中只包含了当前线程的内核模式内存的转储。崩溃转储文件的优点是可以用Windbg直接打开,就像调试内核一样进行调试!并且是支持使用Windbg命令的。

 

我们这里使用了!analyze -v命令,这个命令是用来自动分析出错原因的。我们可以在图中看到错误码是e2

这时候如果你输入栈回溯指令“K”就可以看到触发蓝屏的过程。如图所示

 

通过栈回溯我们可以猜测函数的执行流程。如果你足够敏感,你会发现KiTrap03这一行。

我们都知道int 3是个断点指令,但是对底层不了解的人可能不知道int 3是怎么处理的。这其实涉及到Windows内核对异常的处理方式,Windows内核通过IDT表来查找处理例程,而KiTrap03正是int 3IDT中对应的处理例程。这说明,Windbg是使用了int 3来触发蓝屏的。

一个int 3是怎么导致蓝屏的?我们可以在栈回溯中看到nt!KiDispatchException,这是个内核异常分发函数,它的上面是nt!KdpTrap一个沟通内核调试器函数。就是说Windbg通过在内核模式下触发一个异常使内核沟通到调试器,然后执行了KdpCauseBugCheck触发了蓝屏,这个函数中真正起作用的其实是KeBugCheckEx。接下来这篇文章的重点就是分析这个函数。但是 我们该怎样去获知这个的具体操作流程呢?一种常见的方法就是通过反汇编。然而我并不打算通过反汇编的形式来研究这个函数,原因很简单: 反汇编代码并不容易理解,而且当没有符号文件的情况下更是令人蛋疼。

众所周知的是,Windows是一个不开源的系统,然而我们还是可以通过一些特殊的手段看到Windows的源代码。比如可以借助React OS,一个致力于实现与Windows相同环境的开源系统。Windows内核方面的经典著作《Windows内核情景分析》就是基于React OS的,虽然React OS并不是Windows,但是根据我个人的经验来说,React OS代码与Windows代码并没有本质的区别。另一个途径就是WRK了,WRK的全称是“Windows Research Kernel”,它是微软为高校提供的操作系统教学平台。它给出了Windows操作系统内核的大部分代码,可以对其进行修改、编译,并且可以用这个内核启动Windows操作系统。虽然WRK并不是真正的运行在我们电脑上的操作系统代码,但它是我们能接触到的最近真实代码的源码了。下面我就以最常见的WRK1.2版本来进行操作。我们这里用VS2015打开从网上下载WRK1.2工程,使用VS自带的搜索功能就可以找到KeBugCheck函数,整个过程比较慢,因为WRK内容实在是太大了。我们找到KeBugCheck函数后,会发现这个函数只是简单的对KeBugCheck2函数的封装,

 

可见真正的工作都在KeBugCheck2中完成。而KeBugCheck2是一个相当复杂的函数,呃,至少在代码量上来看是这样的,应该有接近900行。我们跟进这个函数,我们先把注意力放在KeBugCheck2的参数上,第一个参数是BugCheckCode,这个参数实际上就是输出在蓝屏上的神奇的代码,其实这个代码一点也不神奇。因为微软已经给出了他们的官方解释,你可以在MSDN上找到它们。

 

Windows驱动开发有所了解朋友自然对WDK不会陌生,在WDK中也可找到它们的解释。我们跟进这个函数来一探究竟。

 

 1 VOID
 2 KeBugCheck2 (
 3     __in ULONG BugCheckCode,
 4     __in ULONG_PTR BugCheckParameter1,
 5     __in ULONG_PTR BugCheckParameter2,
 6     __in ULONG_PTR BugCheckParameter3,
 7     __in ULONG_PTR BugCheckParameter4,
 8     __in_opt PKTRAP_FRAME TrapFrame
 9     )
10  
11  
12 {
13  
14  
15     if (BugCheckCode == POWER_FAILURE_SIMULATE)
16     {
17         KiScanBugCheckCallbackList();
18         HalReturnToFirmware(HalRebootRoutine);
19 }

 

首先面对的这么一段代码,可见这是对错误代码为POWER_FAILURE_SIMULATE的情况的特殊处理,怎么处理的呢?使用HalReturnToFirmware函数,这个函数实质上是Hal.dll的例程。可见我们真的已经足够底层了,再往下挖就到硬件了:)

这个函数的作用是调用BIOS例程实现重启,虽然很少有人听过这个函数,但是却可能有很多人用过这个函数。因为据说PCHunter(原XueTr)的暴力重启就是使用这个函数实现的。

 

 1 switch (BugCheckCode) {
 2  
 3         case SYSTEM_THREAD_EXCEPTION_NOT_HANDLED:
 4         case KERNEL_MODE_EXCEPTION_NOT_HANDLED:
 5         case KMODE_EXCEPTION_NOT_HANDLED:
 6             PssMessage = KMODE_EXCEPTION_NOT_HANDLED;
 7             break;
 8  
 9         case DATA_BUS_ERROR:
10         case NO_MORE_SYSTEM_PTES:
11         case INACCESSIBLE_BOOT_DEVICE:
12         case UNEXPECTED_KERNEL_MODE_TRAP:
13         case ACPI_BIOS_ERROR:
14         case ACPI_BIOS_FATAL_ERROR:
15         case FAT_FILE_SYSTEM:
16         case DRIVER_CORRUPTED_EXPOOL:
17         case THREAD_STUCK_IN_DEVICE_DRIVER:
18             PssMessage = BugCheckCode;
19             break;
20  
21         case DRIVER_CORRUPTED_MMPOOL:
22             PssMessage = DRIVER_CORRUPTED_EXPOOL;
23             break;
24  
25         case NTFS_FILE_SYSTEM:
26             PssMessage = FAT_FILE_SYSTEM;
27             break;
28  
29         case STATUS_SYSTEM_IMAGE_BAD_SIGNATURE:
30             PssMessage = BUGCODE_PSS_MESSAGE_SIGNATURE;
31             break;
32         default:
33             PssMessage = BUGCODE_PSS_MESSAGE;
34         break;
35 }

 

这是根据错误码来获取最终的错误编码,而这个错误编码就是最终会显示在蓝屏界面上的神秘乱码

我们接着往下看

 1 switch (BugCheckCode) {
 2  
 3     case FATAL_UNHANDLED_HARD_ERROR:
 4     case IRQL_NOT_LESS_OR_EQUAL:
 5     case ATTEMPTED_WRITE_TO_READONLY_MEMORY:
 6     case ATTEMPTED_EXECUTE_OF_NOEXECUTE_MEMORY:
 7     case KERNEL_MODE_EXCEPTION_NOT_HANDLED:
 8     case DRIVER_LEFT_LOCKED_PAGES_IN_PROCESS:
 9     case DRIVER_USED_EXCESSIVE_PTES:
10     case PAGE_FAULT_IN_NONPAGED_AREA:
11     case THREAD_STUCK_IN_DEVICE_DRIVER:
12 }

又是一个以BugCheckCode为条件的switch语句,这个switch语句中针对不同的错误代码进行了详细的设置,比如这行代码

 

ExecutionAddress = (PVOID)BugCheckParameter4;

 

就是用来设置导致崩溃发生的指令地址的,我们在Windbg调试崩溃转储文件时就会看到这个值。Windbg会显示,异常可能是因为XX地址的XX指令导致的,这个XX地址就是由这个ExecutionAddress得来的。

再看看这行代码

  KiBugCheckDriver = &DataTableEntry->BaseDllName;

这个值就是保存导致崩溃的模块的名称的,后面会经常使用到这个值,这个值会被写入崩溃转储文件,同样的Windbg也会输出这个值。接着往下看

 

1 if ((BugCheckCode != MANUALLY_INITIATED_CRASH) &&        (KdDebuggerEnabled)) {
2  
3         DbgPrint("\n*** Fatal System Error: 0x%08lx\n"
4                  "                       (0x%p,0x%p,0x%p,0x%p)\n\n",
5                  (ULONG)KiBugCheckData[0],
6                  KiBugCheckData[1],
7                  KiBugCheckData[2],
8                  KiBugCheckData[3],
9                  KiBugCheckData[4]);

 

这个就是当检测到调试器后就输出错误编码,这时候前面设置的代码就派上了用处,注意这里的条件是不能是MANUALLY_INITIATED_CRASH,而我们用.crash触发的就是这个,所以想看到这个只能去触发一个真正的异常了。如图

我这里触发了一个真正的异常,果然出现DbgPrint的结果。

 

之后会马上调用如下函数

// Freeze execution of the system by disabling interrupts and looping.
KeDisableInterrupts();
KeRaiseIrql(HIGH_LEVEL, &OldIrql);

微软的官方注释已经说明了它的作用:禁用除了当前进程以为其他的一切活动。我来说明这个是怎么实现的,对于CPU来说有一个重要的值叫做IRQL值,高的IRQL值可以屏蔽低的IRQL值。而线程切换是运行于DPC级的IRQL级别上的,而这个函数把IRQL级别提升到了HIGH_LEVEL也就是高于DPC级从而让所有的线程无法切换,实现了屏蔽线程分发。禁用中断则是针对多处理器来说的,屏蔽了多处理器总线。这样一来就保证了,只会有这个处理蓝屏的线程在运行。

接下来继续往下看,会找到这个函数

1  KiDisplayBlueScreen (PssMessage,
2                       HardErrorCalled,
3                       HardErrorCaption,
4                       HardErrorMessage,
5                       AnsiBuffer);

没错,直到此时才是名副其实的蓝屏,这个函数是用来绘制一个蓝屏屏幕的。绘制一个蓝屏后同时会输出我们熟悉的错误信息,每个版本的Windows的具体输出内容有所不同。但是会输出前面获取的那些值,也就是我们看见到这个函数的5个参数。比如PssMessage就是通过前面第一个switch语句来获取值的,它的含义是蓝屏原因或者是蓝屏代码。

紧接着的是

KiInvokeBugCheckEntryCallbacks();

这个函数的用途是调用系统中已注册的崩溃回调函数,Windows系统为驱动程序提供了许多回调函数或叫事件通知。比如进程创建回调函数、模块加载回调函数等等。系统提供崩溃回调的目的应该是用于让用户的驱动程序在退出前来清理资源的。

接着往下看就会发现产生dump文件的步骤,

 

IoWriteCrashDump((ULONG)KiBugCheckData[0],
                         KiBugCheckData[1],
                         KiBugCheckData[2],
                         KiBugCheckData[3],
                         KiBugCheckData[4],
                         &ContextSave,
                         Thread,
                         &Reboot);

 

这个函数就会产生我们在上面用过的蓝屏崩溃转储文件。

这里传入的参数都是由上面的switch语句来获取的。我们前面已经介绍了崩溃转储文件是一份内存状态的备份,其实转储文件不是单纯的备份。它是以特殊的数据结构来组织的,里面保存了不同的数据。这样才能实现直接Windbg进行打开和操作。

下面就是收尾工作了,因为崩溃处理的目的已经完成了,该保存的数据也保存成功了。

if (Reboot) 
{   
        DbgUnLoadImageSymbols (NULL, (PVOID)-1, 0);
        HalReturnToFirmware (HalRebootRoutine);
}

这个是Reboot值取决于用户有没有设置蓝屏后自动重新启动,默认应该是自动重新启动的。

HalRerturnToFirmware就是前面讲过的重启函数。整个函数流程以重启收尾。

 

 至此整个函数的流程已分析完毕

总结一下,Windows蓝屏处理函数首先会根据蓝屏错误码来设置要显示的错误代码和各种状态值如发生错误的模块、发生错误的地址等等,之后禁用掉其他所有线程的运行并且禁止线程分发和中断。并且会寻找内核调试器,如果找到了内核调试器 会输出错误信息并中断到内核调试器。之后就是绘制我们熟悉的蓝屏画面,并且生产崩溃转储文件。最后进行重新启动。自此,这个函数的流程分析的就差不多了,下一次打算写一篇与Windows调试机制有关的文章。

 

 

 

 

 

 

 

 


声明:该文观点仅代表作者本人,转载请注明来自看雪