R3藍屏的多種方式及原理分析(一)
前言
藍屏死機(英語:Blue Screen of Death,縮寫:BSoD)指的是微軟Windows操作系統在無法從一個系統錯誤中恢復過來時所顯示的屏幕圖像。藍屏有它存在的理由,在遇到非常嚴重的嚴重錯誤時,為了避免更嚴重的錯誤,立馬中止系統的所有操作,順便給個提示,讓你知道錯誤的原因,對于一個專業人員來說,這些機制確實是非常重要的。但當你正在寫論文還未保存,眼前卻一片藍色的時候,我想你一定也會黯然神傷吧。這個讓普通人恨之入骨,專業人員愛恨交織的東西,究竟有怎樣的魔力?本文只探討由R3引起的BSoD,明眼人都知道研究R0的藍屏純屬脫褲子放屁。
涉及到的知識點:
- 調用未公開的API實現Ring3的BSoD行為
- 通過對比Reactos源碼挖掘NT內核的更多秘密
- 借助IDA的靜態分析探究BSoD的流程
- windbg的Local Kernel Debugging和雙機調試的各種技巧
下圖為經典的Win10藍屏

背景
相信很多人都對R3下的藍屏很好奇,我也不例外,簡單講幾個用處,可以用于自己程序的反調試手段,不過藍屏比起其他反調試手段顯然太過暴力,屬于殺敵一千自損八百的情況,當然這種手段也比較好破解。對于各安全廠商而言,這應該是需要特別注意的,如何連這都攔截不了,后面的防護也只是癡人說夢。在網上一搜索了一大圈,大部分文章都是介紹API的,里面各種藍屏方式,其實都是一個套路,講解原理的確實少之又少。后來尋了半天終于找到一篇,Ring3觸發BSOD代碼實現及內核逆向分析,我也是受到啟發,寫下了本文,上文主要對Process進行了探究,本文將從各個方面更加深入的探究它的原理機制。
Ring3藍屏的方式
簡單的可以分成3大類,如果還有其他種類的歡迎大家補充,網上各大文章里的手段基本是第二類的衍生,但本質還是利用它是Critical Process,但是無論哪種最后調用的一定是由Ring0調用nt!KeBugCheckEx
第一類藍屏:NtRaiseHardError
特別注意:需要SeShutdownPrivilege權限,因此需要利用RtlAdjustPrivilege提權,但是無需繞過UAC(無需以管理員身份運行)
NTSTATUSNTAPINtRaiseHardError( 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 0x22.SeAssignPrimaryTokenPrivilege 0x33.SeLockMemoryPrivilege 0x44.SeIncreaseQuotaPrivilege 0x55.SeUnsolicitedInputPrivilege 0x06.SeMachineAccountPrivilege 0x67.SeTcbPrivilege 0x78.SeSecurityPrivilege 0x89.SeTakeOwnershipPrivilege 0x910.SeLoadDriverPrivilege 0xa11.SeSystemProfilePrivilege 0xb12.SeSystemtimePrivilege 0xc13.SeProfileSingleProcessPrivilege 0xd14.SeIncreaseBasePriorityPrivilege 0xe15.SeCreatePagefilePrivilege 0xf16.SeCreatePermanentPrivilege 0x1017.SeBackupPrivilege 0x1118.SeRestorePrivilege 0x12 19.SeShutdownPrivilege 0x13 20.SeDebugPrivilege 0x1421.SeAuditPrivilege 0x1522.SeSystemEnvironmentPrivilege 0x1623.SeChangeNotifyPrivilege 0x1724.SeRemoteShutdownPrivilege 0x1825.SeUndockPrivilege 0x1926.SeSyncAgentPrivilege 0x1a27.SeEnableDelegationPrivilege 0x1b28.SeManageVolumePrivilege 0x1c29.SeImpersonatePrivilege 0x1d30.SeCreateGlobalPrivilege 0x1e31.SeTrustedCredManAccessPrivilege 0x1f32.SeRelabelPrivilege 0x2033.SeIncreaseWorkingSetPrivilege 0x2134.SeTimeZonePrivilege 0x2235.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的封裝
第三類藍屏:系統資源耗盡
將物理內存占滿應該可以實現藍屏,讀者可以自行測試,不是本文講解的重點
NtRaiseHardError逆向分析
0.實現代碼
先直接上藍屏代碼,通過代碼來進行分析
#include #include // 需要的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線程掛靠,導致藍屏

總結
整個流程大致如下圖所示

由此看來,NtRaiseHardError的藍屏“旅程”并不復雜,后面的進程線程藍屏才是真正的挑戰,由于文章篇幅限制,只能放到下一節內容來進行講解,后續的分析會比這個更加深入,更加曲折,更加刺激!
