0x00 前言
蓝屏死机
(英语:Blue Screen of Death,缩写:BSoD)指的是微软Windows操作系统在无法从一个系统错误中恢复过来时所显示的屏幕图像。蓝屏有它存在的理由,在遇到非常严重的严重错误时,为了避免更严重的错误,立马中止系统的所有操作,顺便给个提示,让你知道错误的原因,对于一个专业人员来说,这些机制确实是非常重要的。但当你正在写论文还未保存,眼前却一片蓝色的时候,我想你一定也会黯然神伤吧。这个让普通人恨之入骨,专业人员爱恨交织的东西,究竟有怎样的魔力?本文只探讨由R3引起的BSoD,明眼人都知道研究R0的蓝屏纯属脱裤子放屁。
涉及到的知识点:
- 调用未公开的API实现Ring3的BSoD行为
- 通过对比Reactos源码挖掘NT内核的更多秘密
- 借助IDA的静态分析探究BSoD的流程
- windbg的Local Kernel Debugging和双机调试的各种技巧
0x01 背景
相信很多人都对R3下的蓝屏很好奇,我也不例外,简单讲几个用处,可以用于自己程序的反调试手段,不过蓝屏比起其他反调试手段显然太过暴力,属于杀敌一千自损八百的情况,当然这种手段也比较好破解。对于各安全厂商而言,这应该是需要特别注意的,如何连这都拦截不了,后面的防护也只是痴人说梦。在网上一搜索了一大圈,大部分文章都是介绍API的,里面各种蓝屏方式,其实都是一个套路,讲解原理的确实少之又少。后来寻了半天终于找到一篇,Ring3触发BSOD代码实现及内核逆向分析,我也是受到启发,写下了本文,上文主要对Process进行了探究,本文将从各个方面更加深入的探究它的原理机制。
0x02 Ring3蓝屏的方式
简单的可以分成3大类,如果还有其他种类的欢迎大家补充,网上各大文章里的手段基本是第二类的衍生,但本质还是利用它是Critical Process,但是无论哪种最后调用的一定是由Ring0调用nt!KeBugCheckEx
第一类蓝屏:NtRaiseHardError
特别注意:需要SeShutdownPrivilege
权限,因此需要利用RtlAdjustPrivilege
提权,但是无需绕过UAC(无需以管理员身份运行)
NTSTATUS
NTAPI
NtRaiseHardError(
IN NTSTATUS ErrorStatus, // 错误代码
IN ULONG NumberOfParameters, // 指定了Parameters指针数组包含的指针个数
IN PUNICODE_STRING UnicodeStringParameterMask OPTIONAL, // 该参数的各个二进制位与Paramenters指针数组一一对应,如果某一位为1,则说明对应的参数指针指向的是一个UNICODE_STRING,否则是一个整数
IN PVOID *Parameters, // 参数
IN HARDERROR_RESPONSE_OPTION ResponseOption, // 枚举类型 本文使用6号,具体含义见下面的代码
OUT PHARDERROR_RESPONSE Response // 返回值
);
以下是RtlAdjustPrivilege
可获取的各种权限,单独提出来的2个是需要用到的
1.SeCreateTokenPrivilege 0x2
2.SeAssignPrimaryTokenPrivilege 0x3
3.SeLockMemoryPrivilege 0x4
4.SeIncreaseQuotaPrivilege 0x5
5.SeUnsolicitedInputPrivilege 0x0
6.SeMachineAccountPrivilege 0x6
7.SeTcbPrivilege 0x7
8.SeSecurityPrivilege 0x8
9.SeTakeOwnershipPrivilege 0x9
10.SeLoadDriverPrivilege 0xa
11.SeSystemProfilePrivilege 0xb
12.SeSystemtimePrivilege 0xc
13.SeProfileSingleProcessPrivilege 0xd
14.SeIncreaseBasePriorityPrivilege 0xe
15.SeCreatePagefilePrivilege 0xf
16.SeCreatePermanentPrivilege 0x10
17.SeBackupPrivilege 0x11
18.SeRestorePrivilege 0x12
19.SeShutdownPrivilege 0x13
20.SeDebugPrivilege 0x14
21.SeAuditPrivilege 0x15
22.SeSystemEnvironmentPrivilege 0x16
23.SeChangeNotifyPrivilege 0x17
24.SeRemoteShutdownPrivilege 0x18
25.SeUndockPrivilege 0x19
26.SeSyncAgentPrivilege 0x1a
27.SeEnableDelegationPrivilege 0x1b
28.SeManageVolumePrivilege 0x1c
29.SeImpersonatePrivilege 0x1d
30.SeCreateGlobalPrivilege 0x1e
31.SeTrustedCredManAccessPrivilege 0x1f
32.SeRelabelPrivilege 0x20
33.SeIncreaseWorkingSetPrivilege 0x21
34.SeTimeZonePrivilege 0x22
35.SeCreateSymbolicLinkPrivilege 0x23
我们可以在自己的电脑上查看一下自己的权限
普通用户权限
绕过UAC时的权限
第二类蓝屏:Critical Process/Thread
如果对于系统启动过程非常熟悉的话,应该听说过一些系统的关键进程如csrss.exe挂了的话,则整个系统必然挂,这就是属于这一类蓝屏的特色了,只要某个线程或者进程挂了,整个系统就会蓝屏,所以你可能会想,那我只要把这种进程线程关了不就可以蓝吗?那确实,但是你的想法肯定也早就在微软的考虑中,对于那些系统线程,在EHTREAD的SystemThread位置将会是1,这下好了,在关闭线程的函数里判断SystemThread的值,如果是,我就不关,这你不就没办法了吗,对于这部分在后面的逆向部分会提到。咱们先来验证一下是否能关闭系统进程
那不如换种思路,既然无法将系统的Critical Process/Thread关掉,那我把自己设置成Critical Process/Thread“身份”,然后把自己关掉总可以了吧。
我们需要以下API来改变自己的“身份”,以下均为导出但未文档化的函数(在ntdll中),因此可以使用GetProcAddress直接获取,不必搜索特征码,倒是省了一个大麻烦。但是需要SeDebugPrivilege
权限,上面可以看到,那是通过UAC之后才有的权限,因此必须绕过UAC才能使用以下API
NtSetInformationProcess
NtSetInformationThread
-
RtlSetProcessIsCritical
实际是对NtSetInformationProcess的封装 -
RtlSetThreadIsCritical
实际是对NtSetInformationThread的封装
第三类蓝屏:系统资源耗尽
将物理内存占满应该可以实现蓝屏,读者可以自行测试,不是本文讲解的重点
0x03 NtRaiseHardError的逆向分析
0.实现代码
先直接上蓝屏代码,通过代码来进行分析
#include <stdio.h>
#include <windows.h>
// 需要的Shutdown权限,至于为什么是19看上面RtlAdjustPrivilege的介绍
const ULONG SE_SHUTDOWN_PRIVILEGE = 19;
typedef struct _UNICODE_STRING
{
USHORT Length;
USHORT MaximumLength;
PWCH Buffer;
}UNICODE_STRING, *PUNICODE_STRING;
typedef enum _HARDERROR_RESPONSE_OPTION
{
OptionAbortRetryIgnore,
OptionOk,
OptionOkCancel,
OptionRetryCancel,
OptionYesNo,
OptionYesNoCancel,
OptionShutdownSystem
} HARDERROR_RESPONSE_OPTION, *PHARDERROR_RESPONSE_OPTION;
typedef enum _HARDERROR_RESPONSE
{
ResponseReturnToCaller,
ResponseNotHandled,
ResponseAbort,
ResponseCancel,
ResponseIgnore,
ResponseNo,
ResponseOk,
ResponseRetry,
ResponseYes
} HARDERROR_RESPONSE, *PHARDERROR_RESPONSE;
// 函数指针
typedef NTSTATUS(NTAPI *NTRAISEHARDERROR)(
IN NTSTATUS ErrorStatus,
IN ULONG NumberOfParameters,
IN PUNICODE_STRING UnicodeStringParameterMask OPTIONAL,
IN PVOID *Parameters,
IN HARDERROR_RESPONSE_OPTION ResponseOption,
OUT PHARDERROR_RESPONSE Response
);
typedef BOOL(NTAPI *RTLADJUSTPRIVILEGE)(ULONG, BOOL, BOOL, PBOOLEAN);
HARDERROR_RESPONSE_OPTION ResponseOption = OptionShutdownSystem;
HARDERROR_RESPONSE Response;
NTRAISEHARDERROR NtRaiseHardError;
RTLADJUSTPRIVILEGE RtlAdjustPrivilege;
int main()
{
// 任何进程都会自动加载ntdll,因此直接获取模块地址即可,不必再LoadLibrary
HMODULE NtBase = GetModuleHandle(TEXT("ntdll.dll"));
if (!NtBase) return false;
// 获取各函数地址
NtRaiseHardError = (NTRAISEHARDERROR)GetProcAddress(NtBase, "NtRaiseHardError");
RtlAdjustPrivilege = (RTLADJUSTPRIVILEGE)GetProcAddress(NtBase, "RtlAdjustPrivilege");
// 提权
BOOLEAN B;
if (!RtlAdjustPrivilege(SE_SHUTDOWN_PRIVILEGE, TRUE, FALSE, &B) == 0)
{
printf("提权失败");
getchar();
return 0;
}
NTSTATUS status = NtRaiseHardError(0xC0000217, 0, NULL, NULL, OptionShutdownSystem, &Response);
return 0;
}
开启双击调试,直接蓝,连调试的机会都不给你,可以说确实很无解了,至少咱们后面的分析的Critical Thread是可以被windbg断下来的。这个函数本来是用来显示错误信息的,现在却被用来干这种事,世事难料,安全的对抗是永无止境的。
1.栈回溯-观察整体
先通过栈回溯观察下程序的运行流程,便于后续的分析,在KeWaitForSingleObject
返回地址处下个断点,直接蓝了,说明该函数就是蓝屏的“元凶”,这个函数非常复杂,它具体干了咱们不用管,在这个例子中大致就是等待服务程序插入线程来处理它的关机请求。
2.IDA分析NtRaiseHardError
分析基于20H1版本的内核,在win7 x64以上这些函数基本没啥变化。
为了方便理解,下文用PX来代指NtRaiseHardError的参数,a代表当前函数的参数,P1就是NtRaiseHardError的第一个参数,a5则为当前函数的第五个参数。
由于第四个参数为0因此直接跳过中间大部分步骤,直接开始调用ExpRaiseHardError()
,P1 P2 P3 a5分别是NtRaiseHardError传进来参数1、2、3、5,Dst和v26是一个局部变量数组int64[5],v22则用于返回Response至a6。当然如果你在R0调用,则PreviousMode=0会走下面那个分支调用ExRaiseHardError()
下面让我们看看ExpRaiseHardError干了什么
3.ExpRaiseHardError
不要看上面的参数,是错的,流程却是对的,IDA的F5果然还是不靠谱,算了,还是手动分析参数吧。
事实证明,千万不能信任IDA的F5,重要环节还得自己来,用windbg验证一下,bp nt!ExpSystemErrorHandler
下个断点,64位函数使用rcx rdx r8 r9来传递前四个参数的值,因此rcx rdx r8 r9应该分别是P1 P2 P3 v26的值,可以发现完全符合,证明我们分析的参数是正确的。
4.ExpSystemErrorHandler
这函数没做什么事,直接调用了ExpSystemErrorHandler2()
,依旧不需要看上面的参数顺序,并不正确,经过分析跟ExpSystemErrorHandler
的参数完全一样
ExpSystemErrorHandler2(P1, P2, P3, v26数组地址, a5);
5.ExpSystemErrorHandler2
ExpSystemErrorHandler2
进行一些列字符串的操作,然后调用了PoShutdownBugCheck
,第三个参数是最初的P1(错误码)。我们通过栈回溯可以知道,并不会执行下面的KeBugCheckEx
,因为调用PoShutdownBugCheck
时就已经蓝屏了。
6.PoShutdownBugCheck
没做什么有用的事,将函数分发给了ZwInitiatePowerAction,大家也不要看见KeBugCheckEx
就兴奋,下面的KeBugCheckEx
依旧没有执行的机会。
Nt是给R0调用的,Nt系列的更底层,Zw系列的函数是给R3调用的,需要做一些检查,不管怎么说最后调用的一定是Nt的,所以咱们直接分析Nt的。
7.NtInitiatePowerAction
前面一大堆加锁、去锁,咱们不管它,看关键步骤,执行到这里,调用KeWaitForSingleObject
然后等待system线程挂靠,导致蓝屏
0x4 总结
整个流程大致如下图所示
由此看来,NtRaiseHardError
的蓝屏“旅程”并不复杂,后面的进程线程蓝屏才是真正的挑战,由于文章篇幅限制,只能放到下一节内容来进行讲解,后续的分析会比这个更加深入,更加曲折,更加刺激!
0x5 参考
Reactos源码
Ring3触发BSOD代码实现及内核逆向分析
软件调试 第2版 卷2 Windows平台调试 (上) 第十三章 硬错误和蓝屏