這篇文章本來是投Freebuf的,結果沒過。就貼到博客里吧,圖懶得發上來了
對於Windows系統來說,被人們視為洪水猛獸的藍屏也是一種有利於系統穩定的機制。藍屏其實是Windows系 統的一種自查機制,一但系統發現自己哪里有些不對勁后就立即拋出藍屏,來阻止錯誤蔓延。倘若沒有藍屏機制,那么可能很小的一個錯誤最后會不斷的醞釀導致系 統數據損壞的嚴重后果。而事實上因為Windows系統自身導致的藍屏其實是少之又少的,更多的藍屏誘因是各種驅動程序,因為作者個人對Rootkit類 程序感興趣,因此在平時的學習過程中深感各種不良的內核HOOK或者過濾驅動是誘發藍屏的小能手。當然不符合微軟規定的編程方式或是軟件BUG,比如常見 的IRQL錯誤和違反PatchGuard也會觸發藍屏。當我們理解了Windows藍屏機制的重要意義之后,一個新的問題被提出來了,就是Windows藍屏究竟是如何產生的呢?
首先我們可以做一個觸發藍屏的實驗,用Windbg和VMware虛 擬機進行雙機調試,首先打開被調試虛擬機。虛擬機停留在啟動菜單選項時選擇以調試模式啟動,其實這內核提供的一個功能,如果以這種配置啟動 Windows,內核會通過串口向外尋找遠端調試器,因為是虛擬機雙機調試所以是啟用的虛擬串口。所謂的啟動菜單其實就是bootmgr程序,這個程序是 由MBR直接啟動的。而且這可能是整個Windows系統中最奇葩的PE程序了,這個程序的一部分是實模式的指令一部分是保護模式的指令。bootmgr 會先執行實模式的部分,啟動實模式的指令會把CPU狀態轉到保護模式,於是程序的保護模式指令開始啟動,之后bootmgr會啟動winload.exe 來進行系統內核的加載。
Windbg成功掛載到內核后,內核會自動中斷到Windbg調試器,可能很多人都只是輸入G直接繼續執行了。但是其實這里是很有搞頭的,我們可以在Windbg中輸入K指令來看一下棧回溯。
如圖,此時內核其實是很初始的階段,我們看到KiSystemStartup這個函數是內核初始化的主要函數,然后是初始化內核核心和內核執行體。如 果是只接觸過linux內 核的朋友可能會有疑問,什么叫內核核心和內核執行體?其實這種划分來自於微軟的定義。內核核心是內核中較低層的部分,實現基本的 功能。而內核執行體則是內核中較為上層的部分,我們常接觸的就是這部分,各種管理器比如對象管理器、進程管理器也都在這部分。通過棧回溯我們看到中斷時內 核處於剛剛初始化的階段,而此時我們有一個絕佳的機會去跟蹤內核的啟動流程,如果有機會我會寫一篇調試Windows內核初始化的文章。
我們回到正題,我們的目的是觸發一次藍屏然后跟蹤藍屏的產生流程。那么如何觸發一次藍屏呢?寫一個驅動可以達到這個目的,但是太麻煩了,而且很多讀者可能並沒有接觸過驅動開發。其實Windbg的一條命令就可以實現觸發藍屏,而且甚至MSDN都給出了方法
我們在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 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調試機制有關的文章。
