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.不过在终结之前,系统再次调用发生异常的线程中所有的异常处理过程,这是线程异常处理过程获得的最后清理未释放资源的机会,其后程序就终结了