內核學習-異常處理

異常產生后,首先是要記錄異常信息(異常的類型、異常發生的位置等),然后要尋找異常的處理函數,稱為異常的分發,最后找到異常處理函數并調用,稱為異常處理。
圍繞 異常處理,異常分發,異常處理 展開。
異常的分類:
(1)cpu產生的異常

代碼執行時,CPU檢測到除數為零,cpu拋出異常。
(2)軟件模擬產生的異常
在C++或者是C#等一些高級語言中,在程序需要的時候也可以主動拋出異常,這種高級語言拋出的異常就是模擬產生的異常,并不是真正的異常。

異常記錄
CPU異常記錄
1、CPU異常的處理流程
① CPU指令檢測到異常(例:除零)
② 查IDT表(如下表),執行中斷處理函數
③ CommonDispatchException
④ KiDispatchException


找中斷表的0號處理函數。
截圖百度百科:

IDA打開ntoskrnl.exe 搜索字符串 _IDT 進行定位。
IDT表:

2、分析中斷處理函數 _KiTrap00
執行流程:
① 保存現場(將寄存器堆棧位置等信息保存到 _Trap_Frame 中)
② 調用CommonDispatchException函數


4018c7函數:沒有對異常進行處理,函數內部調用CommonDispatchException函數。

CommonDispatchException函數內部調用_KiDispatchExceptio,CPU這么設計的目的是為了讓程序員有機會對異常進行處理。

總結:
(1)異常處理函數中并沒有直接對異常進行處理,而是調用了CommonDispatchException。
(2)這樣設計異常的目的是為了程序員有機會對異常進行處理。
3、CommonDispatchException函數分析
該函數構造一個_EXCEPTION_RECORD結構體 并賦值。

CommonDispatchException主要就做了一件事情,就是將異常相關的信息存儲到一個_EXCEPTION_RECORD結構體里,這個結構體的作用就是用來記錄異常信息。
type struct _EXCEPTION_RECORD { DWORD ExceptionCode; //異常代碼 DWORD ExceptionFlags; //異常狀態 struct _EXCEPTION_RECORD* ExceptionRecord; //下一個異常 PVOID ExceptionAddress; //異常發生地址 DWORD NumberParameters; //附加參數個數 ULONG_PTR ExceptionInformation [EXCEPTION_MAXIMUM_PARAMETERS]; //附加參數指針 }

ebx中存儲的是發生異常時的EIP,eax:0C0000094h ,這個是CPU自己在內部定義的異常代碼。
ExceptionFlags異常狀態:通過異常狀態可以區分是CPU產生的異常還是軟件模擬產生的異常。所有的CPU產生的異常這個位置的值是0,所有軟件模擬產生的異常這個位置存儲的值是1,如果堆棧錯誤里面存儲的值是8,如果出現嵌套異常,里面存儲的值是0x10。
ExceptionRecord下一個異常:通常是空的,但是出現嵌套異常,通過指針指向下一個異常。
4、總結
CPU異常的執行流程:
(1)CPU指令檢測到異常
(2)查IDT表,執行中斷處理函數
(3)調用CommonDispatchException(構建EXCEPTION_RECORD結構體)
(4)KiDispatchException(分發異常:目的是為了找到異常處理函數)
模擬異常記錄
示例代碼:通過代碼拋出異常 throw關鍵字
當通過軟件拋出異常的時候,實際上就是調用了CxxThrowException。

CxxThrowException調用(KERNEL32.DLL)RaiseException
RaiseException分析
在堆棧里構建一個EXCEPTION_RECORD結構體,然后對結構體進行賦值。

CPU產生的異常和軟件模擬產生的異常都填充EXCEPTION_RECORD結構體,但是兩者有差異。
差異:
ExceptionCode 異常代碼
當CPU產生異常時會記錄一個ErrorCode,通過查表可以查到ErrorCode具體的含義,不同的異常對應不同的錯誤代碼,但是軟件拋出的ErrorCode是根據編譯環境決定的,如下圖的EDX中存儲的值即為當前編譯環境的ErrorCode。

ExceptionAddress 異常發生地址
第二個區別就是ExceptionAddress,CPU異常記錄的位置是真正的異常發生時的地址。

CPU記錄異常的地址是真正發生異常的地址,軟件模擬產生的異常里面存儲的是RaiseException這個函數的地址。
RaiseException函數分析
RaiseException調用RtlRaiseException (ntdll.dll)

RtlRaiseException調用_ZwRaiseException

_ZwRaiseException調用NtRaiseException

NtRaiseException調用_KiRaiseException
這個函數主要做了兩件事:
(1)把EXCEPTION_RECORD結構體的ExceptionCode最高位清零,用于區分CPU異常。
(2)調用KiDispatchException開始分發異常。


RaiseException在初始化一個EXCEPTION_RECORD結構體之后,開始調用NTDLL中的RtlRaiseException; 之后,開始調用內核中NtRaiseException, NtRaiseException再調用另外一個內核函數KiRaiseException。接下來KiRaiseException會調用KiDispatchException開始異常的分發
https://www.cnblogs.com/Winston/archive/2010/03/16/1687649.html
http://runxinzhi.com/Ox9A82-p-5374527.html
cpu異常和模擬異常只有在異常記錄不同,調用KiDispatchException開始異常的分發后步驟都相同。
異常處理
內核層用戶處理流程
異常可以發生在用戶空間,也可以發生在內核空間。無論是CPU異常還是模擬異常,是用戶層異常還是內核層異常,都要通過KiDispatchException函數進行分發。
VOID KiDispatchException(ExceptionRecord, ExceptionFrame,TrapFrame,PreviousMode, FirstChance)
(1)KiDispatchException函數分析
KiDispatchException函數執行流程總結:
① 將Trap_Frame備份到context為返回三環做準備;
② 判斷先前模式 0是內核調用 1是用戶層調用;
③ 判斷是否是第一次調用;
④ 判斷是否有內核調試器;
⑤ 如果沒有內核調試器則不處理;
⑥ 調用RtlDispatchException處理異常;
⑦ 如果RtlDispatchException返回FALSE,再次判斷是否有內核調試器,沒有直接藍屏。
(ntoskrnl.dll)
將Trap_frame備份到context為返回3環做準備(由于操作系統不知道分發的異常到底是3環的異常還是0環的異常,如果是3環的異常,進0環前的環境會被備份到Trap_Frame中,如果要在中途回到3環的話,就需要將 Trap_frame 備份到 context,為返回3環做準備)。


如果沒有內核調試器或者內核調試器不處理,都跳轉到loc_4309A1。
調用_RtlDispatchException處理異常:

如果RtlDispatchException返回FALSE,再次判斷是否有內核調試器,沒有直接藍屏。



(2)RtlDispatchException函數分析
RtlDispatchException函數作用:遍歷異常鏈表,調用異常處理函數,如果異常被正確處理了,該函數返回1。如果當前異常處理函數不能處理該異常,那么調用下一個,以此類推。如果到最后也沒有人處理這個異常,返回0。
RtlDispatchException調用_RtlpGetRegistrationHead

RtlpGetRegistrationHead將FS:0保存到eax之后返回。我們知道FS:0在零環的時候指向的是KPCR,而KPCR的第一個成員就是ExceptionList(具體信息可看以往文章apc和系統調用)。
ExceptionList,指向了一個結構體 _EXCEPTION_REGISTRATION_RECORD
_EXCEPTION_REGISTRATION_RECORD結構體必須位于當前線程的堆棧中。
typedef struct _EXCEPTION_REGISTRATION_RECORD{ struct _EXCEPTION_REGISTRATION_RECORD *Next; PEXCEPTION_ROUTINE Handler;} EXCEPTION_REGISTRATION_RECORD;
這個結構體有兩個成員,第一個成員指向下一個_EXCEPTION_REGISTRATION_RECORD,如果沒有下一個_EXCEPTION_REGISTRATION_RECORD結構體,那么這個地方的值是-1。第二個成員是異常處理函數。
當調用RtlDispatchException時,按順序執行異常處理函數,若其中一個異常處理函數返回結果為真,就不再繼續向下執行。
若執行完所有異常處理函數后,異常仍然沒有被處理,那么就返回FALSE。

用戶層異常處理流程
(1)上述內核層異常處理中,異常處理函數也在0環,不用切換堆棧,用戶層異常發生在三環,異常處理函數也在3環,所以要切換堆棧(因為KiDispatchException在內核,從0環返到三環)回到3環執行異常處理函數。
(2)切換堆棧的處理方式與用戶APC的執行過程幾乎是一樣的,惟一的區別就是執行用戶APC時返回3環后執行的函數是KiUserApcDispatcher,而異常處理時返回3環后執行的函數是KiUserExceptionDispatcher。
(3)理解用戶APC的執行過程是理解3環異常處理的關鍵。
分析用戶層異常發生時的 KiDispatchException
Trap_Frame備份到context。

判斷previousmode內核調用 還是 用戶層調用,如果是用戶層調用,jnz到loc_42COC2。

當不存在內核調試器或者內核調試器沒有處理時,跳轉到42C10A。

調用DbgkForwardException函數將異常發送給3環調試器。

3環調試器如果不存在或者沒有處理的話,就會開始修改寄存器,準備返回3環。

這里eax的值是一個全局變量KeUserExceptionDispatcher;在操作系統初始化的時候,會給這個全局變量賦一個值,這個值就是ntdll.KiUserExceptionDispatcher函數。
總結:
(1)_KeContextFromKframes將Trap_frame被分到context為返回3環做準備
(2)判斷席安全模式,0是內核調用,1是用戶層調用
(3)是否都是第一次機會
(4)否有內核調試器
(5)發送給3環調試
(6)如果3環調試器沒有處理這個異常,修正EIP為KiUserExceptionDispatcher
(7)KiUserExceptionDispatcher函數執行結束:CPU異常與模擬異常返回地點不同
(8)無論通過那種方式,但線程再次回到3環時,將執行KiUserExceptionDispatcher函數
VEH(向量化異常處理)
當用戶異常產生后,內核函數KiDispatchException并不是像處理內核異常那樣在0環直接處理,而是修正3環EIP為KiUserExceptionDispatcher函數后就結束了。
這樣,當線程再次回到3環時,將會從KiUserExceptionDispatcher函數開始執行。
KiUserExceptionDispatcher函數分析

調用_RtlDispatchException找到當前異常的處理函數,處理成功,再次調用ZwContinue重新進入0環(回到3環時,修改了eip,將返回地址更改為當前函數,因此進入0環需要再次修正eip,ZwContinue再次進入零環目的就是把修正后的context寫進trap_frame里,再次返回3環后,就會從修正后的位置再次執行)。

沒有找到當前的處理函數,跳轉,調用_ZwRaiseException對異常進行第二次分發。
3環調用了RtlCallVectoredExceptionHandlers,0環沒有調用此函數。

_RtlDispatchException函數分析
作用:
(1)查找VEH鏈表(全局鏈表),如果有則調用,如果沒有繼續查找局部鏈表
(2)查找SEH鏈表(局部鏈表,在堆棧中),如果有則調用
注意:與內核調用時的區別!
VEH:全局鏈表,無論哪一個線程出現問題,都會先找這個全局鏈表,VEH中沒有異常處理函數,在查找SEH鏈表。
代碼:自定義VEH
typedef PVOID(NTAPI *FnAddVectoredExceptionHandler)(ULONG, _EXCEPTION_POINTERS*);FnAddVectoredExceptionHandler MyAddVectoredExceptionHandler;// VEH異常處理只能返回2個值// EXCEPTION_CONTINUE_EXECUTION 已處理// EXCEPTION_CONTINUE_SEARCH 未處理 //定義VEH的異常處理函數VectExcepHandler,這個函數只能有兩個返回值LONG NTAPI VectExcepHandler(PEXCEPTION_POINTERS pExcepInfo){ MessageBox(NULL,L"VEH異常處理函數執行了...",L"VEH異常",MB_OK); if (pExcepInfo->ExceptionRecord->ExceptionCode == 0xC0000094)//異常被觸發,判斷是否是除0異常 { //1.修改發生異常的代碼的Eip idiv ecx長度2字節 從下一行開始執行 pExcepInfo->ContextRecord->Eip = pExcepInfo->ContextRecord->Eip + 2; //2.將除數修改為1 //pExcepInfo->ContextRecord->Ecx = 1; return EXCEPTION_CONTINUE_EXECUTION;//已處理 } return EXCEPTION_CONTINUE_SEARCH;//未處理} int main(){ //動態獲取AddVectoredExceptionHandler函數地址 //AddVectoredExceptionHandler:將異常處理函數插入到VEH全局鏈表中 HMODULE hModule = GetModuleHandle(L"Kernel32.dll"); MyAddVectoredExceptionHandler = (FnAddVectoredExceptionHandler)::GetProcAddress(hModule,"AddVectoredExceptionHandler"); //參數1表示插入VEH鏈的頭部, 0插入到VEH鏈的尾部 MyAddVectoredExceptionHandler(0, (_EXCEPTION_POINTERS *)&VectExcepHandler); //構造除0異常 int val = 0; _asm { xor edx, edx xor ecx, ecx mov eax, 100 idiv ecx //edx = eax / ecx //異常觸發,執行寫入的異常處理函數 mov val, edx } printf("val = %d",val); getchar();}
這個異常處理函數的參數是一個結構體指針,有兩個成員。
typedef struct _EXCEPTION_POINTERS{ PEXCEPTION_RECORD ExceptionRecord;//異常發生時的信息 PCONTEXT ContextRecord;//異常發生時的上下文環境} EXCEPTION_POINTERS, *PEXCEPTION_POINTERS;
有了這個參數我們就可以捕獲異常發生時的相關信息并且修改異常發生時的寄存器環境。
代碼中是先判斷異常代碼是否為除0異常,然后修改發生異常的Eip和Ecx,接著返回異常已處理。如果不是除0異常就返回異常未處理。
VEH異常的處理流程
① CPU捕獲異常
② 通過KiDispatchException進行分發(3環異常將EIP修改為 KiUserExceptionDispatcher)
③ KiUserExceptionDispatcher調用RtlDispatchException
④ RtlDispatchException查找VEH處理函數鏈表,并調用相關處理函數
⑤ 代碼返回到ZwContinue再次進入0環
⑥ 線程再次返回3環后,從修正的位置開始執行
SEH(結構化異常處理)
當用戶異常產生后,內核函數KiDispatchException并不是像處理內核異常那樣在0環直接進行處理,而是修正3環EIP為KiUserExceptionDispatcher函數后就結束了。
這樣,當線程再次回到3環時,將會從KiUserExceptionDispatcher函數開始執行。
KiUserExceptionDispatcher會調用RtlDispatchException函數來查找并調用異常處理函數,查找順序:
① 先查全局鏈表:VEH
② 再查局部鏈表:SEH
SEH是線程相關的,存儲在當前線程的堆棧中。
三環,FS:0指向的是TEB,TEB的第一個成員是一個子結構體_NT_TIB,這個子結構體的第一個成員就是ExceptionList,異常處理鏈表。
typedef struct _EXCEPTION_REGISTRATION_RECORD{ struct _EXCEPTION_REGISTRATION_RECORD* Next; //下一個節點,-1就是沒有下一個節點了 PEXCEPTION_ROUTINE Handler; //指向下一個SEH異常處理函數} EXCEPTION_REGISTRATION_RECORD;

VEH異常處理,全局的鏈表,想要插入異常處理函數,調用AddVectoredExceptionHandler函數;
SEH異常處理,想要插入異常處理函數的線程,需要自行插入_EXCEPTION_REGISTRATION_RECORD 這中結構。
RtlDispatchException函數分析








handler必須遵循調用約定,有自己的格式。
/*SEH處理函數是提供給RtlDispatchException調用的,所以需要遵循一定的格式。EXCEPTION_DISPOSITION _cdecl MyEexception_handler( struct _EXCEPTION_RECORD *ExceptionRecord, //異常結構體 PVOID EstablisherFrame, //SEH結構體地址 struct _CONTEXT *ContextRecord, //存儲異常發生時的各種寄存器的值 棧位置等 PVOID DispatcherContext)*/
自定義SEH
#include #include /*//0環異常處理時講過這個結構體typedef struct _EXCEPTION_REGISTRATION_RECORD{ struct _EXCEPTION_REGISTRATION_RECORD *Next; PEXCEPtiON_ROUTINE Handler;}*/ struct MyException{ struct MyException *prev; DWORD handler;};/*SEH處理函數是提供給RtlDispatchException調用的,所以需要遵循一定的格式。EXCEPTION_DISPOSITION _cdecl MyEexception_handler( struct _EXCEPTION_RECORD *ExceptionRecord, //異常結構體 PVOID EstablisherFrame, //SEH結構體地址 struct _CONTEXT *ContextRecord, //存儲異常發生時的各種寄存器的值 棧位置等 PVOID DispatcherContext)*/EXCEPTION_DISPOSITION __cdecl MyExceptionHandler( struct _EXCEPTION_RECORD *ExceptionRecord, //ExceptionRecord存儲異常信息:什么類型、異常產生位置 void * EstablisherFrame, //MyException結構體地址 struct _CONTEXT *ContextRecord, //Context結構體,存儲異常發生時各種寄存器的值,堆棧位置等 void * Dispatchercontext){ MessageBox(NULL,L"SEH異常處理函數執行了...",L"SEH異常",NULL); if (ExceptionRecord->ExceptionCode == 0xC0000094) { ContextRecord->Eip = ContextRecord->Eip + 2; ContextRecord->Ecx = 100; return ExceptionContinueExecution; } return ExceptionContinueSearch;} void TestException(){ DWORD temp; //插入異常,必須在當前線程的堆棧當中 //若定義成全局變量則無效 MyException myException; __asm { mov eax, FS:[0] mov temp, eax lea ecx, myException mov FS:[0], ecx } //鏈表插入操作,將原來的也掛入到插入的SEH后面 myException.prev = (MyException*)temp; myException.handler = (DWORD)&MyExceptionHandler; //構造除0異常 __asm { xor edx, edx xor ecx, ecx mov eax, 0x10 idiv ecx //EDX:EAX 除以 ECX } //處理完成,摘掉異常 __asm { mov eax, temp mov FS:[0], eax } printf("函數執行完畢");} int main(){ TestException(); return 0;}
SEH異常的處理流程
① FS:[0]指向SEH鏈表的第一個成員;
② SEH的異常處理函數必須在當前線程的堆棧中;
③ 只有當VEH中的異常處理函數不存在或者不處理才會到SEH鏈表中查找。
編譯器擴展的SEH

SEH的處理流程實際上就是在當前的堆棧中掛一個鏈表。
使用SEH這種方式來處理異常要創建一個結構體,掛到當前TEB的ExceptionList鏈表里,編寫異常處理函數。
編譯器支持的SEH
_try // 掛入鏈表{ }_except(過濾表達式) // 異常過濾{ 異常處理程序 }
except里的過濾表達式用于異常過濾,只能有以下三個值:
EXCEPTION_EXECUTE_HANDLER(1) 異常已經被識別,控制流將進入到 _except模塊中運行異常處理代碼。
EXCEPTION_CONTINUE_SEARCH(0) 異常不被識別,也即當前的這個 _except模塊不是這個異常錯誤所對應的正確的異常處理模塊。系統將繼續到上 _try except域中繼續查找一個恰當的 _except模塊。
EXCEPTION_CONTINUE_EXECUTION(-1) 異常被忽略,控制流將在異常出現的點之后,繼續恢復運行。
過濾表達式只能有三種寫法:
① 直接寫常量值
② 表達式
③ 調用函數
手動掛入鏈表:
_asm{ mov eax,FS:[0] mov temp,eax lea ecx,myException mov FS:[0],ecx }