技术心得:分析Windows的死亡蓝屏(BSOD)机制

简介: 技术心得:分析Windows的死亡蓝屏(BSOD)机制

  这篇文章本来是投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
相关文章
|
25天前
|
Windows
windows系统bat批处理 清理注册表与蓝屏补丁
windows系统bat批处理 清理注册表与蓝屏补丁
12 1
|
28天前
|
消息中间件 程序员 Windows
Windows消息机制《MFC深度详解》
Windows消息机制《MFC深度详解》
19 1
|
1月前
|
前端开发 Linux iOS开发
【Flutter前端技术开发专栏】Flutter在桌面应用(Windows/macOS/Linux)的开发实践
【4月更文挑战第30天】Flutter扩展至桌面应用开发,允许开发者用同一代码库构建Windows、macOS和Linux应用,提高效率并保持平台一致性。创建桌面应用需指定目标平台,如`flutter create -t windows my_desktop_app`。开发中注意UI适配、性能优化、系统交互及测试部署。UI适配利用布局组件和`MediaQuery`,性能优化借助`PerformanceLogging`、`Isolate`和`compute`。
【Flutter前端技术开发专栏】Flutter在桌面应用(Windows/macOS/Linux)的开发实践
|
1月前
|
监控 安全 网络协议
windows服务器权限分析
windows服务器权限分析
36 1
windows服务器权限分析
|
1月前
|
安全 Linux 开发工具
Linux与Windows系统的差异分析
Linux与Windows系统的差异分析
46 0
|
1月前
|
Java Android开发 Windows
Java for Windows Missing问题原因分析以及完美解决方案
Java for Windows Missing问题原因分析以及完美解决方案
25 0
|
1月前
|
Linux Windows
Windows Server 下文件同步
Windows Server 下文件同步
35 0
|
18天前
|
编解码 安全 网络安全
RealVNC的 VNC server在windows7系统下无法正确运行
在Windows 7上运行旧版VNC Server(如4.1.2)可能存在兼容性问题,但可通过调整配置解决。步骤包括:安装VNC Server,设置兼容性模式(选择Windows XP SP3),启动VNC Server,配置VNC连接参数。若遇到问题,检查防火墙设置,确保系统更新,并考虑升级到新版VNC Server以提高性能和兼容性。
|
1月前
|
开发框架 .NET API
在Windows Server 2008 R2上运行.Net 8应用
在Windows Server 2008 R2上成功运行.Net 8程序,需安装三个补丁:Windows Server 2008 R2 SP1 (KB976932)是基础更新;VC_redist.x64提供MSVC库支持;KB3063858解决.NET运行时加载`kernel.dll`的路径问题。KB3063858可能需要KB2533623。详细信息和下载链接在文中给出。