分析Windows的死亡藍屏(BSOD)機制

Ox9A82發表於2016-06-06

  這篇文章本來是投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除錯機制有關的文章。

 

 

 

 

 

 

 

 

相關文章