这篇文章本来是投Freebuf的,结果没过。就贴到博客里吧,图懒得发上来了
对于Windows系统来说,被人们视为洪水猛兽的蓝屏也是一种有利于系统稳定的机制。蓝屏其实是Windows系 统的一种自查机制,一但系统发现自己哪里有些不对劲后就立即抛出蓝屏,来阻止错误蔓延。倘若没有蓝屏机制,那么可能很小的一个错误最后会不断的酝酿导致系 统数据损坏的严重后果。而事实上因为Windows系统自身导致的蓝屏其实是少之又少的,更多的蓝屏诱因是各种驱动程序,因为作者个人对Rootkit类 程序感兴趣,因此在平时的学习过程中深感各种不良的内核HOOK或者过滤驱动是诱发蓝屏的小能手。当然不符合微软规定的编程方式或是软件BUG,比如常见 的IRQL错误和违反PatchGuard也会触发蓝屏。当我们理解了Windows蓝屏机制的重要意义之后,一个新的问题被提出来了,就是Windows蓝屏究竟是如何产生的呢?
首先我们可以做一个触发蓝屏的实验,用Windbg和VMware虚 拟机进行双机调试,首先打开被调试虚拟机。虚拟机停留在启动菜单选项时选择以调试模式启动,其实这内核提供的一个功能,如果以这种配置启动 Windows,内核会通过串口向外寻找远端调试器,因为是虚拟机双机调试所以是启用的虚拟串口。所谓的启动菜单其实就是bootmgr程序,这个程序是 //代码效果参考:http://www.lyjsj.net.cn/wz/art_23560.html
由MBR直接启动的。而且这可能是整个Windows系统中最奇葩的PE程序了,这个程序的一部分是实模式的指令一部分是保护模式的指令。bootmgr 会先执行实模式的部分,启动实模式的指令会把CPU状态转到保护模式,于是程序的保护模式指令开始启动,之后bootmgr会启动winload.exe 来进行系统内核的加载。Windbg成功挂载到内核后,内核会自动中断到Windbg调试器,可能很多人都只是输入G直接继续执行了。但是其实这里是很有搞头的,我们可以在Windbg中输入K指令来看一下栈回溯。
如图,此时内核其实是很初始的阶段,我们看到KiSystemStartup这个函数是内核初始化的主要函数,然后是初始化内核核心和内核执行体。如 果是只接触过linux内 核的朋友可能会有疑问,什么叫内核核心和内核执行体?其实这种划分来自于微软的定义。内核核心是内核中较低层的部分,实现基本的 功能。而内核执行体则是内核中较为上层的部分,我们常接触的就是这部分,各种管理器比如对象管理器、进程管理器也都在这部分。通过栈回溯我们看到中断时内 核处于刚刚初始化的阶段,而此时我们有一个绝佳的机会去跟踪内核的启动流程,如果有机会我会写一篇调试Windows内核初始化的文章。
我们回到正题,我们的目的是触发一次蓝屏然后跟踪蓝屏的产生流程。那么如何触发一次蓝屏呢?写一个驱动可以达到这个目的,但是太麻烦了,而且很多读者可能并没有接触过驱动开发。其实Windbg的一条命令就可以实现触发蓝屏,而且甚至MSDN都给//代码效果参考:http://www.lyjsj.net.cn/wz/art_23558.html
出了方法我们在Windbg中输入G,让虚拟机继续执行。等系统启动完毕后,用Windbg的Ctrl+Break抛出断点使系统中断到Windbg中。
在Windbg调试器中输入.crash,系统就会触发蓝屏。如图
没错,这个就是当前最“时尚潮流“的蓝屏,与以前传统的蓝屏相比简直就是高富帅和屌丝的差别,但是其实无论是高富帅蓝屏还是屌丝蓝屏其实内部流程都是一样的,只是绘制出的图形不一样而已。
通过.crash命令触发的蓝屏会导致系统重启,我们是不能在调试器中获得通知的,这个时候就需要使用崩溃转储分析了。当你的Windows发生蓝屏崩溃后,系统会自动的储存一份转储文件在你的硬盘中,这份转储与我们通常调试程序时建立的dump文件是相似的,如图就是我用.crash命令触发蓝屏后 形成的转储文件。
注意转储文件的命名是以月日年-排号的顺序来命名的。我是在4月17写的这篇文章,而这是今天的第一个崩溃转储,所以命名就是041716-01,Mini代表 迷你转储。转储文件其实就是崩溃发生时内存状态的一个备份,系统把它封装成一定的格式然后保存起来。Window提供了三种不同的类型的转储,其中Mini转储的体积是最小的,当然内容也是最少的。Mini转储中只包含了当前线程的内核模式内存的转储。崩溃转储文件的优点是可以用Windbg直接打开,就像调试内核一样进行调试!并且是支持使用Windbg命令的。
我们这里使用了!analyze -v命令,这个命令是用来自动分析出错原因的。我们可以在图中看到错误码是e2。
这时候如果你输入栈回溯指令“K”就可以看到触发蓝屏的过程。如图所示
通过栈回溯我们可以猜测函数的执行流程。如果你足够敏感,你会发现KiTrap03这一行。
我们都知道int 3是个断点指令,但是对底层不了解的人可能不知道int 3是怎么处理的。这其实涉及到Windows内核对异常的处理方式,Windows内核通过IDT表来查找处理例程,而KiTrap03正是int 3在IDT中对应的处理例程。这说明,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 //代码效果参考:http://www.lyjsj.net.cn/wz/art_23556.html
&nbs