VMPROTECT處理異常2-各種異常處理(VEH,VCH,SEH,UEF)
- 2020 年 4 月 6 日
- 筆記
VEH,VCH,SEH,UEF
VEH: 向量化異常處理程式(進程相關)
VCH: 同上,也是向量化異常處理程式,不過它總是在最後被調用(進程相關)
SEH: 結構化異常處理程式,這個不用解釋了吧。就是fs:[0]那個(執行緒相關)
UEF: 即TopLevalEH,基於SEH的,是進程相關
因為SEH的的頭部被保存在TEB(fs:[0]),所以它是執行緒相關的
UEF、VEH、VCH異常處理函數定義(UEF和VEH、VCH的函數類型名不一樣,但是參數是一樣的):
typedef struct _EXCEPTION_POINTERS { PEXCEPTION_RECORD ExceptionRecord; PCONTEXT ContextRecord; } EXCEPTION_POINTERS, *PEXCEPTION_POINTERS;
函數調用
SEH
#define GetExceptionInformation (struct _EXCEPTION_POINTERS *)_exception_info LPEXCEPTION_POINTERS GetExceptionInformation(void);
關於GetExceptionInformation函數,要記住的最重要事情是它只能在異常過濾器中調用,因為僅僅在處理異常過濾器時,CONTEXT、EXCEPTION_RECORD和EXCEPTION_POINTERS才是有效的。一旦控制被轉移到異常處理程式,棧中的數據就被刪除。
示例1:
DWORD SEHTest() { DWORD dwTemp = 0; EXCEPTION_RECORD SavedExceptRec; CONTEXT SavedContext; __try { dwTemp = 5/dwTemp;//異常 } __except(SavedExceptRec = *(GetExceptionInformation())->ExceptionRecord, SavedContext = *(GetExceptionInformation())->ContextRecord, EXCEPTION_EXECUTE_HANDLER) { //寫這裡面是錯誤的!:SavedExceptRec = *(GetExceptionInformation())->ExceptionRecord; //寫這裡面是錯誤的!SavedContext = *(GetExceptionInformation())->ContextRecord; int k = 0; } return dwTemp; }
異常過濾器是指__except()的括弧中,下面這樣寫也是錯誤的:
EXCEPTION_RECORD SavedExceptRec; CONTEXT SavedContext; LONG SEHFilter() { //寫這裡面是錯誤的!:SavedExceptRec = *(GetExceptionInformation())->ExceptionRecord; //寫這裡面是錯誤的!SavedContext = *(GetExceptionInformation())->ContextRecord; return EXCEPTION_EXECUTE_HANDLER; } DWORD SEHTest() { DWORD dwTemp = 0; __try { dwTemp = 5/dwTemp;//異常 } __except(SEHFilter()) { int k = 0; } return dwTemp; }
UEF
LPTOP_LEVEL_EXCEPTION_FILTER WINAPI SetUnhandledExceptionFilter( __in LPTOP_LEVEL_EXCEPTION_FILTER lpTopLevelExceptionFilter );
UEF只有一個有效,後面的會覆蓋前面的,這個函數的返回值就是前面的設置地址,最重要的是,如果進程被調試,無論調試器是否處理異常,UEF都不會被執行
示例2:
LONG __stdcall UEFFilter(_EXCEPTION_POINTERS* ExceptionInfo) { printf("UEFn"); return EXCEPTION_EXECUTE_HANDLER; } LONG __stdcall UEFFilter0(_EXCEPTION_POINTERS* ExceptionInfo) { printf("UEF0n"); return EXCEPTION_EXECUTE_HANDLER; } int _tmain(int argc, _TCHAR* argv[]) { SetUnhandledExceptionFilter(&UEFFilter0); LPTOP_LEVEL_EXCEPTION_FILTER lp = SetUnhandledExceptionFilter(&UEFFilter); lp(NULL);// 調用前一個設置函數
如示例2,列印的結果是
UEF0
VEH
PVOID WINAPI AddVectoredExceptionHandler( __in ULONG FirstHandler,//為0,則回調位於VEH鏈表的尾部,非0則置於VEH鏈表的最前端 __in PVECTORED_EXCEPTION_HANDLER VectoredHandler);
VEH需要自己移除:
ULONG WINAPI RemoveVectoredExceptionHandler( __in PVOID Handler);
VEH是先於SEH執行的!
一個VEH可以返回連個值:EXCEPTION_CONTINUE_SEARCH、EXCEPTION_CONTINUE_EXECUTION。返回EXCEPTION_EXECUTE_HANDLER是無效的,等同於EXCEPTION_CONTINUE_SEARCH。
當一個Veh返回EXCEPTION_CONTINUE_SEARCH,則把異常交給下一個VEH處理。
如果返回EXCEPTION_CONTINUE_EXECUTION,認為已經被處理,退出異常處理器在異常指令處繼續執行。
示例3:
DWORD dwTemp = 0; DWORD SEHTest() { __try { dwTemp = 15/dwTemp;//異常 } __except(MessageBoxA(NULL,"SEH",NULL,0),// 此處不會執行到,因為VEH已修復了dwTemp EXCEPTION_CONTINUE_SEARCH ) { } return dwTemp;// 最後輸出結果為15/5=3 } LONG __stdcall VEHFilter0(_EXCEPTION_POINTERS* ExceptionInfo) { MessageBoxA(NULL,"VEH0",NULL,0); dwTemp = 5;// 修復BUG return EXCEPTION_CONTINUE_EXECUTION;// 處理完,重新執行dwTemp = 15/dwTemp; } int _tmain(int argc, _TCHAR* argv[]) { AddVectoredExceptionHandler(1,&VEHFilter0);
VEH和UEF的區別:
1.VEH可以指定多個,UEF只能指定一個
2.VEH可以指定異常處理是否處理鏈的最前面
VEH和UEF的相同:
1.都是進程相關,而不是執行緒相關
2.若VEH和SEH回調都未處理異常,最後系統要進行展開,但不會調用VEH和UEF
VCH
PVOID WINAPI AddVectoredContinueHandler( __in ULONG FirstHandler, __in PVECTORED_EXCEPTION_HANDLER VectoredHandler);
函數和VEH一個樣,同樣需要自己移除
ULONG WINAPI RemoveVectoredContinueHandler( __in PVOID Handler);
VCH是最後執行的(在UEF之後),1.無調試器狀態下,當異常被處理,並且返回EXCEPTION_CONTINUE_EXECUTION時,會觸發VCH 2.有調試器狀態下,會觸發VCH
其餘都不會觸發VCH
示例4:
DWORD dwTemp = 0; DWORD SEHTest() { dwTemp = 15/dwTemp;//異常 return dwTemp;// 最後輸出結果為15/5=3 } LONG __stdcall VEHFilter(_EXCEPTION_POINTERS* ExceptionInfo) { MessageBoxA(NULL,"VEH",NULL,0); dwTemp = 5; return EXCEPTION_CONTINUE_EXECUTION;//如果改為EXCEPTION_EXECUTE_HANDLER,不在調試狀態下運行,不會彈VCH框!! } LONG __stdcall VCHFilter(_EXCEPTION_POINTERS* ExceptionInfo) { MessageBoxA(NULL,"VCH",NULL,0); return EXCEPTION_EXECUTE_HANDLER; } int _tmain(int argc, _TCHAR* argv[]) { AddVectoredExceptionHandler(1,&VEHFilter); AddVectoredContinueHandler(1,&VCHFilter);
上例中會觸發VCH彈框,但是,如果把VEHFilter的返回值改為EXCEPTION_EXECUTE_HANDLER,不在調試狀態下運行,不會彈VCH框,在調試狀態(VS或WINDBG之類),會彈VCH框
相關結構體
SEH:
EXCEPTION_DISPOSITION __cdecl _except_handler ( _In_ struct _EXCEPTION_RECORD *_ExceptionRecord,//異常記錄結構指針 _In_ void * _EstablisherFrame,//指向EXCEPTION_REGISTRATION結構,即SEH鏈 _Inout_ struct _CONTEXT *_ContextRecord,//Context結構指針 (執行緒上下文) _Inout_ void * _DispatcherContext//調度器上下文 );
VEH、UEF、VCH的參數都是
typedef struct _EXCEPTION_POINTERS { PEXCEPTION_RECORD ExceptionRecord;//異常記錄(EXCEPTION_RECORD)的指針 PCONTEXT ContextRecord;//執行緒上下文的指針 } EXCEPTION_POINTERS, *PEXCEPTION_POINTERS;
EXCEPTION_RECORD
EXCEPTION_RECORD結構包含有關已發生異常的獨立於CPU的資訊
typedef struct _EXCEPTION_RECORD { DWORD ExceptionCode;// ExceptionCode包含異常的程式碼,等同於GetExceptionCode()的值 DWORD ExceptionFlags; struct _EXCEPTION_RECORD *ExceptionRecord; PVOID ExceptionAddress; DWORD NumberParameters; ULONG_PTR ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS]; } EXCEPTION_RECORD;
ExceptionFlags: 包含有關異常的標誌。當前只有兩個值,分別是0(指出一個可以繼續的異常)和EXCEPTION_NONCONTINUABLE(指出一個不可繼續的異常)。在一個不可繼續的異常之後,若要繼續執行,會引發一個EXCEPTION_NONCONTINUABLE_EXCEPTION異常
ExceptionRecord: 指向另一個未處理異常的EXCEPTION_RECORD結構。在處理一個異常的時候,有可能引發另外一個異常。例如,異常過濾器中的程式碼就可能用零來除一個數。當嵌套異常發生時,可將異常記錄鏈接起來,以提供另外的資訊。如果在處理一個異常過濾器的過程當中又產生一個異常,就發生了嵌套異常。如果沒有未處理異常,這個成員就包含一個NULL。
ExceptionAddress:指出產生異常的CPU指令的地址
NumberParameters:規定了與異常相聯繫的參數數量(0到15)。這是在ExceptionInformation數組中定義的元素數量。對幾乎所有的異常來說,這個值都是零。
ExceptionInformation:規定一個附加參數的數組,用來描述異常。對大多數異常來說,數組元素是未定義的。
EXCEPTION_RECORD結構的最後兩個成員,NumberParameters和ExceptionInformation向異常過濾器提供一些有關異常的附加資訊。目前只有一種類型的異常提供附加資訊,就是EXCEPTION_ACCESS_VIOLATION。所有其他可能的異常都將NumberParameters設置成零。我們可以檢驗ExceptionInformation的數組成員來查看關於所產生異常的附加資訊。 對於一個EXCEPTION_ACCESS_VIOLATION異常來說,ExceptionInformation[0]包含一個標誌,指出引發這個存取異常的操作的類型。如果這個值是0,表示執行緒試圖要讀不可訪問的數據。如果這個值是1,表示執行緒要寫不可訪問的數據。ExceptionInformation[1]指出不可訪問數據的地址。
CONTEXT
這個結構是依賴於平台的,也就是說,對於不同的CPU平台,這個結構的內容也不一樣,以下只討論X86的
typedef struct _CONTEXT { //-->ContextFlags成員用於向GetThreadContext函數指明你想檢索哪些暫存器,注意GetThreadContext要先暫停執行緒 DWORD ContextFlags; //-->CONTEXT_DEBUG_REGISTER用於標識CPU的調試暫存器 DWORD Dr0; DWORD Dr1; DWORD Dr2; DWORD Dr3; DWORD Dr6; DWORD Dr7; //-->ONTEXT_FLOATING_POINT用於標識CPU的浮點暫存器 FLOATING_SAVE_AREA FloatSave; //-->CONTEXT_SEGMENTS用於標識CPU的段暫存器 DWORD SegGs; DWORD SegFs; DWORD SegEs; DWORD SegDs; //-->CONTEXT_INTEGER用於標識CPU的整數暫存器 DWORD Edi; DWORD Esi; DWORD Ebx; DWORD Edx; DWORD Ecx; DWORD Eax; // -->CONTEXT_CONTROL包含CPU的控制暫存器,比如指令指針、堆棧指針、標誌和函數返回地址 DWORD Ebp; DWORD Eip; DWORD SegCs; // MUST BE SANITIZED DWORD EFlags; // MUST BE SANITIZED DWORD Esp; DWORD SegSs; //-->CONTEXT_EXTENDED_REGISTERS用於標識CPU的擴展暫存器 BYTE ExtendedRegisters[MAXIMUM_SUPPORTED_EXTENSION]; } CONTEXT;
流程圖
我們來分析下整個流程圖:
EXCEPTION_CONTINUE_EXECUTION (–1) Exception is dismissed. Continue execution at the point where the exception occurred. EXCEPTION_CONTINUE_SEARCH (0) Exception is not recognized. Continue to search up the stack for a handler, first for containing try-except statements, then for handlers with the next highest precedence. EXCEPTION_EXECUTE_HANDLER (1) Exception is recognized. Transfer control to the exception handler by executing the __except compound statement, then continue execution after the __except block.
EXCEPTION_CONTINUE_EXECUTION:如果返回該值,系統會恢復傳給EH的CONTEXT,重新執行引發異常的指令 EXCEPTION_CONTINUE_SEARCH:搜索下一個EH(異常處理函數) EXCEPTION_EXECUTE_HANDLER:繼續處理,如果是_except,則繼續執行__except括弧以後的正常指令
測試1:(VEH,VCH,UEF,SEH的執行順序,不能在調試器下直接運行,因為UEF不能在調試器下運行)
DWORD dwTemp = 0; DWORD SEHTest() { __try { dwTemp = 15/dwTemp;//異常 } __except(MessageBoxA(NULL,"SEH",NULL,0), EXCEPTION_CONTINUE_SEARCH) { } return dwTemp; } LONG __stdcall VEHFilter(_EXCEPTION_POINTERS* ExceptionInfo) { MessageBoxA(NULL,"VEH",NULL,0); return EXCEPTION_CONTINUE_SEARCH; } LONG __stdcall VCHFilter(_EXCEPTION_POINTERS* ExceptionInfo) { MessageBoxA(NULL,"VCH",NULL,0); dwTemp = 5;// 最後修復 return EXCEPTION_CONTINUE_SEARCH; } LONG __stdcall UEFFilter(_EXCEPTION_POINTERS* ExceptionInfo) { MessageBoxA(NULL,"UEF",NULL,0); return EXCEPTION_CONTINUE_EXECUTION;//必須有一個返回EXCEPTION_CONTINUE_EXECUTION或在調試器狀態下,才能觸發VCH } int _tmain(int argc, _TCHAR* argv[]) { SetUnhandledExceptionFilter(&UEFFilter); AddVectoredExceptionHandler(1,&VEHFilter); AddVectoredContinueHandler(1,&VCHFilter); SEHTest();
彈框流程為VEH–>SEH–>UEF–>VCH
示例2:(增加調試器程式,假定示例1為test.exe)
int _tmain(int argc, _TCHAR* argv[]) { STARTUPINFO si = { 0 }; si.cb = sizeof(si); PROCESS_INFORMATION pi = { 0 }; if (CreateProcess( TEXT("G:\test\Debug\test.exe"), NULL,NULL,NULL,FALSE, DEBUG_ONLY_THIS_PROCESS | CREATE_NEW_CONSOLE, NULL,NULL,&si,&pi) == FALSE) { return -1; } DEBUG_EVENT debugEvent; BOOL waitEvent = TRUE; while (WaitForDebugEvent(&debugEvent, INFINITE)) { switch (debugEvent.dwDebugEventCode) { case EXCEPTION_DEBUG_EVENT:// 發生異常時觸發 { MessageBoxA(NULL,"Debug",NULL,0); break; } case EXIT_PROCESS_DEBUG_EVENT: { waitEvent = FALSE; break; } default: break; } if (waitEvent == TRUE) { // DBG_EXCEPTION_NOT_HANDLED 表示未處理,DBG_CONTINUE表示已處理 ContinueDebugEvent(debugEvent.dwProcessId, debugEvent.dwThreadId, DBG_EXCEPTION_NOT_HANDLED); } else { break; } } CloseHandle(pi.hThread); CloseHandle(pi.hProcess); return 0;
彈框流程為Debug->Debug->VEH->SEH->VCH->Debug->崩潰(UEF在調試下不會出現)
而且跟進VS可以發現,前兩次Debug,dwFirstchance置為1,最後一次Debug,dwFirstchance置為0
從而,可以大約得出流程圖如下:

1.因為有多種異常,系統首先判斷異常是否應發送給目標程式,如果應該發送,並且目標程式正處於被調試狀態,則系統掛起程式,填寫如下結構:
typedef struct _EXCEPTION_DEBUG_INFO { EXCEPTION_RECORD ExceptionRecord; DWORD dwFirstChance; } EXCEPTION_DEBUG_INFO, *LPEXCEPTION_DEBUG_INFO;
將成員dwFirstchance置為1,並向調試器發送EXCEPTION_DEBUG_EVENT消息
2.如果調試器未能處理異常或程式根本沒有被調試,系統就會查找VEH鏈,如果存在,則交由它處理
3.如果VEH鏈的某個處理了異常,並且返回EXCEPTION_CONTINUE_EXECUTION,則查找VCH鏈,如有,則執行VCH,再執行程式(VEH只有兩種返回值,要麼是EXCEPTION_CONTINUE_SEARCH表示未處理,其餘值都表示EXCEPTION_CONTINUE_EXECUTION)
4.如果VEH鏈未處理了異常,系統就會查找SEH鏈,如果存在,則交由它處理
5.如果SEH鏈的某個處理了異常,並且返回EXCEPTION_CONTINUE_EXECUTION,則查找VCH鏈,如有,則執行VCH,再執行程式
6.如果SEH鏈的某個處理了異常,並且不返回EXCEPTION_CONTINUE_EXECUTION,則執行程式
7.如果SEH鏈未處理異常,且程式未被調試,則查找UEF項,如果存在,則交由它處理
8.如果UEF處理了異常,並且返回EXCEPTION_CONTINUE_EXECUTION,則查找VCH鏈,如有,則執行VCH,再執行程式
9.如果UEF處理了異常,並且不返回EXCEPTION_CONTINUE_EXECUTION,則執行程式
10.如果UEF未處理異常,且程式不被調試,則調用默認系統處理,程式結束
11.如果程式被調試(UEF無效),則將成員dwFirstchance置為0,並向調試器發送EXCEPTION_DEBUG_EVENT消息
12.如果程式沒有設置進程相關的異常處理過程或者進程相關的異常處理過程也未能處理這個異常,系統會調用默認的系統異常處理程式,通常顯示一個對話框,可以選擇「確定」或者最後將其附加到調試器上的「取消」按鈕。如果沒有調試器能被附加於其上或調試器還是處理不了異常,系統就調用ExitProcess終結程式
13.不過在終結之前,系統再次調用發生異常的執行緒中所有的異常處理過程,這是執行緒異常處理過程獲得的最後清理未釋放資源的機會,其後程式就終結了