先声明一下,本文重点是讲VCH的。
可能在SEH的介绍上有一些误区,请各位大牛体谅,也感谢各位指出我的错误。
最近在搞的SR,因为涉及到Windows下的异常处理机制。
所以研究了下,看了很多的文章,有了不少收获,但是也遇到了一些问题。
所以把我的一些经验和心得写成本文,发出来学习、交流一下。
本文重点讲的是向量化异常处理程序。
首先,先简单解释下这几个名词:
VEH: 向量化异常处理程序(进程相关)
VCH: 同上,也是向量化异常处理程序,不过它总是在最后被调用(进程相关)
SEH: 结构化异常处理程序,这个不用解释了吧。就是fs:[0]那个(线程相关)
UEF: 即TopLevalEH,基于SEH的,是进程相关,但是没测试过
其次再解释什么是EH(异常处理程序):
EH全称就是ExceptionHandler,中文意为异常处理器。
EH(异常处理程序)是做什么的呢,就是当程序发生一些错误、异常时,系统会保存好线程的CONTEXT(线程上下文)。
再交给EH来处理异常,有时候不仅仅是错误、异常。一些调试用的中断,异常处理程序也可以处理。比如int 1、int 3。
因为SEH的的头部被保存在TEB(fs:[0]),所以它是线程相关的。
UEF、VEH、VCH异常处理函数定义(UEF和VEH、VCH的函数类型名不一样,但是结构是一样的):
LONG NTAPI ExceptionHandler(struct _EXCEPTION_POINTERS *ExceptionInfo);
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 //无意义 (调度器上下文?)
);
UEF、VEH、VCH的异常处理函数调用约定是stdcall的,windows下的系统api、回调,基本都是stdcall的。
SEH的异常处理函数调用约定cdecl的。
EH(异常处理程序)的结构,UEF、VEH、VCH都一样的,但是VEH、VCH和UEF异常处理程序不同的一点就是返回值,见下文详解。
先讲UEF、VEH、VCH函数的参数。
参数只有一个,是指向结构_EXCEPTION_POINTERS的指针。具体结构如下:
typedef struct _EXCEPTION_POINTERS {
PEXCEPTION_RECORD ExceptionRecord; //异常记录(EXCEPTION_RECORD)的指针
PCONTEXT ContextRecord; //线程上下文的指针
} EXCEPTION_POINTERS, *PEXCEPTION_POINTERS;
typedef struct _EXCEPTION_RECORD {
DWORD ExceptionCode; //异常代码,说明是什么异常,比如单步、除零、断点等等
DWORD ExceptionFlags; //异常标志
struct _EXCEPTION_RECORD *ExceptionRecord; //指向下一个异常记录(EXCEPTION_RECORD)的指针
PVOID ExceptionAddress; //发生异常的地址
DWORD NumberParameters; //异常信息的个数(即数组ExceptionInformation的个数)
ULONG_PTR ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS]; //异常信息数组
} EXCEPTION_RECORD;
typedef EXCEPTION_RECORD *PEXCEPTION_RECORD;
以下是发生异常线程的cpu各个寄存器的值(线程上下文),就不解释了。
/* 浮点寄存器 */
typedef struct _FLOATING_SAVE_AREA {
DWORD ControlWord;
DWORD StatusWord;
DWORD TagWord;
DWORD ErrorOffset;
DWORD ErrorSelector;
DWORD DataOffset;
DWORD DataSelector;
BYTE RegisterArea[SIZE_OF_80387_REGISTERS];
DWORD Spare0;
} FLOATING_SAVE_AREA;
typedef FLOATING_SAVE_AREA *PFLOATING_SAVE_AREA;
typedef struct _CONTEXT {
//
// The flags values within this flag control the contents of
// a CONTEXT record.
//
// If the context record is used as an input parameter, then
// for each portion of the context record controlled by a flag
// whose value is set, it is assumed that that portion of the
// context record contains valid context. If the context record
// is being used to modify a threads context, then only that
// portion of the threads context will be modified.
//
// If the context record is used as an IN OUT parameter to capture
// the context of a thread, then only those portions of the thread's
// context corresponding to set flags will be returned.
//
// The context record is never used as an OUT only parameter.
//
DWORD ContextFlags;
//
// This section is specified/returned if CONTEXT_DEBUG_REGISTERS is
// set in ContextFlags. Note that CONTEXT_DEBUG_REGISTERS is NOT
// included in CONTEXT_FULL.
//
DWORD Dr0;
DWORD Dr1;
DWORD Dr2;
DWORD Dr3;
DWORD Dr6;
DWORD Dr7;
//
// This section is specified/returned if the
// ContextFlags word contians the flag CONTEXT_FLOATING_POINT.
//
FLOATING_SAVE_AREA FloatSave;
//
// This section is specified/returned if the
// ContextFlags word contians the flag CONTEXT_SEGMENTS.
//
DWORD SegGs;
DWORD SegFs;
DWORD SegEs;
DWORD SegDs;
//
// This section is specified/returned if the
// ContextFlags word contians the flag CONTEXT_INTEGER.
//
DWORD Edi;
DWORD Esi;
DWORD Ebx;
DWORD Edx;
DWORD Ecx;
DWORD Eax;
//
// This section is specified/returned if the
// ContextFlags word contians the flag CONTEXT_CONTROL.
//
DWORD Ebp;
DWORD Eip;
DWORD SegCs; // MUST BE SANITIZED
DWORD EFlags; // MUST BE SANITIZED
DWORD Esp;
DWORD SegSs;
//
// This section is specified/returned if the ContextFlags word
// contains the flag CONTEXT_EXTENDED_REGISTERS.
// The format and contexts are processor specific
//
BYTE ExtendedRegisters[MAXIMUM_SUPPORTED_EXTENSION];
} CONTEXT;
typedef CONTEXT *PCONTEXT;
因为SEH的文章很多,所以本文就不说了。
SEH的使用方法:
使用汇编
push handler // 异常处理函数的地址
push fs:[0] // 前一个异常处理函数函数的地址
mov fs:[0], esp // 装入新的SEH链结构
或者C++中的__try、__expect块。
UEF的注册方法:
使用SetUnhandledExceptionFilter函数。
VEH注册和移除的方法:
使用AddVectoredExceptionHandler函数添加。
使用RemoveVectoredExceptionHandler函数移除。
VCH注册和移除的方法:
使用AddVectoredContinueHandler函数。
使用RemoveVectoredContinueHandler函数移除。
结构化异常处理程序(SEH)的返回值:
typedef enum _EXCEPTION_DISPOSITION {
ExceptionContinueExecution, //0
ExceptionContinueSearch, //1
ExceptionNestedException, //2
ExceptionCollidedUnwind //3
} EXCEPTION_DISPOSITION;
结构化异常处理程序(__expect和UEF)可以返回3种值:
EXCEPTION_EXECUTE_HANDLER :(1)
这里要说明一下,很多帖子这里的解释都是 “该异常被处理。从异常处下一条指令继续执行”,个人感觉不太准确
查阅了MSDN之后发现,该值的解释为:
“ Return from UnhandledExceptionFilter and execute the associated exception handler. This usually results in process termination.”
中文翻译为:“返回从UnhandledExceptionFilter并执行相关的异常处理程序。这通常会导致进程终止。”
就是说如果EH(异常处理程序)返回EXCEPTION_EXECUTE_HANDLER,那么通常会导致进程终止。
EXCEPTION_CONTINUE_SEARCH:(0)
这个就是继续搜索执行下一个EH(异常处理程序)。
EXCEPTION_CONTINUE_EXECUTION:(1)
这个是继续执行,如果EH(异常处理程序)返回该值,那么系统(异常调度器)会恢复传递给EH的CONTEXT(线程上下文),并继续执行。
向量化异常处理程序(VEH、VCH)只能返回2种值:
EXCEPTION_CONTINUE_SEARCH:(0)
这个就是继续搜索下一个EH(异常处理程序)。
EXCEPTION_CONTINUE_EXECUTION:(1)
这个是继续执行,如果EH(异常处理程序)返回该值,那么系统(异常调度器)会恢复传递给EH的CONTEXT(线程上下文),并继续执行。
再来说下C++中的__expect和SEH的关系:
__except是对_except_handler函数的封装,__expect里的表达式(可以是函数)返回值是和UEF一样的。
_except_handler函数里会执行__except()里面的代码。根据表达式返回的值。再转换成相应EXCEPTION_DISPOSITION的值。
说的明白一点,就是__except()里的值是3态的。_except_handler返回值是4态的。
再来说下这些个EH(异常处理程序)的调用顺序:
先来个链接
按照上面帖子的说法。这些EH(异常处理程序)的顺序如下所示:
1. 第一次交给调试器(进程必须被调试)
2. 执行VEH
3. 执行SEH
4. UEF (TopLevelEH 进程被调试时不会被执行)
-->这里应该还有个VCH
5. 最后一次交给调试器(上面的异常处理都说处理不了,就再次交给调试器)
6. 调用异常端口通知csrss.exe
其实在4和5之间还有个VCH,不过这个VCH在一般xp下是没有的。在MSDN上,这个API最低的系统版本如下:
客户端:Windows Vista, Windows XP Professional x64 Edition
服务端:Windows Server 2008, Windows Server 2003 with SP1
不过个人认为这个VCH有点很特立独行,如果现在还不能理解这句话,没关系,往下看。
起初我也以为它跟别的异常处理程序没什么区别,仅仅是执行先后的问题。
但是很多人都发现,在支持VCH的系统中,经常VCH不会被触发,而导致进程终止了。
于是写了个程序,用来测试UEF、VEH、VCH。(SEH不是本文重点,UEF也是基于SEH的)。
先上代码:
注意:代码请在支持VCH的系统中运行,另外在MSVC中,需要配置禁用SafeSEH。
禁用SafeSEH方法:项目属性->链接器->高级->映像具有安全异常处理程序 设置为 否 (/SAFESEH:NO)
运行程序时,请不要使用MSVC运行,编译后不要用调试器启动,否则UEF会失效。
#include
#include
#include
#include
#define DISABLE_SEH 0 //是否禁用线程SEH
#define VEH_INT3 0 //配置是VEH还是VCH处理断点异常
#define ENABLE_UEF 1 //是否启用UEF
#define UEF_HANDLE 0 //UEF是否处理掉异常,ENABLE_UEF为0时,该值无效
#define UEF_C_SEARCH 0 //UEF_HANDLE非0时,该值无效,为0时返回EXCEPTION_EXECUTE_HANDLER,非0时返回EXCEPTION_CONTINUE_SEARCH
LONG NTAPI F_ExceptionHandler(struct _EXCEPTION_POINTERS *ExceptionInfo)
{
printf("VEH!");
switch (ExceptionInfo->ExceptionRecord->ExceptionCode)
{
#if VEH_INT3
case EXCEPTION_BREAKPOINT:
printf("√F_ECODE:%08X Int3!\n", ExceptionInfo->ExceptionRecord->ExceptionCode);
ExceptionInfo->ContextRecord->Eip++;
return EXCEPTION_CONTINUE_EXECUTION;
#else
case EXCEPTION_SINGLE_STEP:
printf("√F_ECODE:%08X SingleStep!\n", ExceptionInfo->ExceptionRecord->ExceptionCode);
ExceptionInfo->ContextRecord->EFlags &= 0xFFFFFEFF;
return EXCEPTION_CONTINUE_EXECUTION;
#endif
default:
printf("×F_ECODE:%08X\n", ExceptionInfo->ExceptionRecord->ExceptionCode);
return EXCEPTION_CONTINUE_SEARCH;
}
}
LONG NTAPI L_ExceptionHandler(struct _EXCEPTION_POINTERS *ExceptionInfo)
{
printf("VCH!");
switch (ExceptionInfo->ExceptionRecord->ExceptionCode)
{
#if VEH_INT3
case EXCEPTION_SINGLE_STEP:
printf("√L_ECODE:%08X SingleStep!\n", ExceptionInfo->ExceptionRecord->ExceptionCode);
ExceptionInfo->ContextRecord->EFlags &= 0xFFFFFEFF;
return EXCEPTION_CONTINUE_EXECUTION;
#else
case EXCEPTION_BREAKPOINT:
printf("√L_ECODE:%08X Int3!\n", ExceptionInfo->ExceptionRecord->ExceptionCode);
ExceptionInfo->ContextRecord->Eip++;
return EXCEPTION_CONTINUE_EXECUTION;
#endif
default:
printf("×L_ECODE:%08X\n", ExceptionInfo->ExceptionRecord->ExceptionCode);
return EXCEPTION_CONTINUE_SEARCH;
}
}
#if ENABLE_UEF
LONG NTAPI MyUEF(struct _EXCEPTION_POINTERS *ExceptionInfo)
{
printf("UEF!");
switch (ExceptionInfo->ExceptionRecord->ExceptionCode)
{
#if VEH_INT3
case EXCEPTION_SINGLE_STEP:
#if UEF_HANDLE
printf("√U_ECODE:%08X SingleStep!\n", ExceptionInfo->ExceptionRecord->ExceptionCode);
ExceptionInfo->ContextRecord->EFlags &= 0xFFFFFEFF;
return EXCEPTION_CONTINUE_EXECUTION;
#else
printf("×U_ECODE:%08X\n", ExceptionInfo->ExceptionRecord->ExceptionCode);
#if UEF_C_SEARCH
return EXCEPTION_CONTINUE_SEARCH;
#else
return EXCEPTION_EXECUTE_HANDLER;
#endif
#endif
#else
case EXCEPTION_BREAKPOINT:
#if UEF_HANDLE
printf("√U_ECODE:%08X Int3!\n", ExceptionInfo->ExceptionRecord->ExceptionCode);
ExceptionInfo->ContextRecord->Eip++;
return EXCEPTION_CONTINUE_EXECUTION;
#else
printf("×U_ECODE:%08X\n", ExceptionInfo->ExceptionRecord->ExceptionCode);
#if UEF_C_SEARCH
return EXCEPTION_CONTINUE_SEARCH;
#else
return EXCEPTION_EXECUTE_HANDLER;
#endif
#endif
#endif
default:
printf("×U_ECODE:%08X\n", ExceptionInfo->ExceptionRecord->ExceptionCode);
#if UEF_C_SEARCH
return EXCEPTION_CONTINUE_SEARCH;
#else
return EXCEPTION_EXECUTE_HANDLER;
#endif
}
}
#endif
int _tmain(int argc, _TCHAR* argv[])
{
AddVectoredExceptionHandler(0, F_ExceptionHandler);
AddVectoredContinueHandler(0, L_ExceptionHandler);
#if DISABLE_SEH
__asm xor eax,eax
__asm mov dword ptr fs : [0], eax
#endif
#if ENABLE_UEF
/* 设置uef */
SetUnhandledExceptionFilter(MyUEF);
#endif
printf("准备抛出单步异常\n");
/* 激活TF标志位 */
__asm pushfd
__asm or dword ptr [esp],0x100
__asm popfd
__asm nop //此处单步异常
printf("准备抛出断点异常\n");
/* 触发int3断点 */
__asm int 3 //此处断点异常
__asm nop
return 0;
}
printf("准备抛出单步异常\n");
/* 激活TF标志位 */
__asm pushfd
__asm or dword ptr [esp],0x100
__asm popfd
__asm nop //此处单步异常
printf("准备抛出断点异常\n");
/* 触发int3断点 */
__asm int 3 //此处断点异常
__asm nop
第一次是单步异常、第二次是断点异常。并且可以根据需要配置UEF。
为了方便测试,我定义了几个宏,用来配置和测试程序。
首先配置成:
#define DISABLE_SEH 0 //是否禁用线程SEH
#define VEH_INT3 0 //配置是VEH还是VCH处理断点异常
#define ENABLE_UEF 1 //是否启用UEF
#define UEF_HANDLE 0 //UEF是否处理掉异常,ENABLE_UEF为0时,该值无效
#define UEF_C_SEARCH 0 //UEF_HANDLE非0时,该值无效,为0时返回EXCEPTION_EXECUTE_HANDLER,非0时返回EXCEPTION_CONTINUE_SEARCH
编译后,运行程序(不要使用MSVC运行)。
因为程序会一闪而退,所以我用cmd启动的程序。
先简单介绍下,VEH!和VCH!后面的那个√和×表示的意义。
√表示返回EXCEPTION_CONTINUE_EXECUTION。
×表示返回EXCEPTION_CONTINUE_SEARCH或者EXCEPTION_EXECUTE_HANDLER。
很明显,当引发单步异常时,VEH最先收到并处理了异常,如下代码:
printf("√F_ECODE:%08X SingleStep!\n", ExceptionInfo->ExceptionRecord->ExceptionCode);
ExceptionInfo->ContextRecord->EFlags &= 0xFFFFFEFF; //去掉TF标志位
return EXCEPTION_CONTINUE_EXECUTION; //继续执行
收到异常后,先打印异常信息,然后取消TF标志位,再继续执行代码。
这里跟预料的一样,完全没有UEF什么事了,因为异常已经被处理了。
但是,即使VEH处理了异常,VCH还是会被触发。
然后是执行了int 3,引发断点异常。
这里没有让VEH处理该异常。于是异常会被继续传递下去。
到达UEF,因为配置的UEF_C_SEARCH为0。所以UEF返回了EXCEPTION_EXECUTE_HANDLER。
到这里,进程自杀了VCH也没有被触发。或许你想问为什么,别着急慢慢看。
继续测试,配置成下面这样:
#define DISABLE_SEH 0 //是否禁用线程SEH
#define VEH_INT3 0 //配置是VEH还是VCH处理断点异常
#define ENABLE_UEF 1 //是否启用UEF
#define UEF_HANDLE 0 //UEF是否处理掉异常,ENABLE_UEF为0时,该值无效
#define UEF_C_SEARCH 1 //UEF_HANDLE非0时,该值无效,为0时返回EXCEPTION_EXECUTE_HANDLER,非0时返回EXCEPTION_CONTINUE_SEARCH
编译后,运行程序(不要使用MSVC运行)。
结果如下图:
这回因为UEF_C_SEARCH设置成1了,所以UEF会返回EXCEPTION_CONTINUE_SEARCH。
但是结果似乎没什么区别,除了多了个进程停止的对话框。
再继续测试,配置成下面这样:
#define DISABLE_SEH 0 //是否禁用线程SEH
#define VEH_INT3 0 //配置是VEH还是VCH处理断点异常
#define ENABLE_UEF 1 //是否启用UEF
#define UEF_HANDLE 1 //UEF是否处理掉异常,ENABLE_UEF为0时,该值无效
#define UEF_C_SEARCH 1 //UEF_HANDLE非0时,该值无效,为0时返回EXCEPTION_EXECUTE_HANDLER,非0时返回EXCEPTION_CONTINUE_SEARCH
编译后,运行程序(不要使用MSVC运行)。
结果如下图:
这回因为UEF_HANDLE设置成了1,所以UEF_C_SEARCH就无效了。并且UEF会处理断点异常,如下面所示:
printf("√U_ECODE:%08X Int3!\n", ExceptionInfo->ExceptionRecord->ExceptionCode);
ExceptionInfo->ContextRecord->Eip++; //跳过int 3
return EXCEPTION_CONTINUE_EXECUTION;
这次VCH终于现身了。但是UEF已经把异常处理完了。
可能你已经有所了解了,但是别着急,还有最后一个测试。
配置成如下所示:
#define DISABLE_SEH 0 //是否禁用线程SEH
#define VEH_INT3 0 //配置是VEH还是VCH处理断点异常
#define ENABLE_UEF 0 //是否启用UEF
#define UEF_HANDLE 1 //UEF是否处理掉异常,ENABLE_UEF为0时,该值无效
#define UEF_C_SEARCH 1 //UEF_HANDLE非0时,该值无效,为0时返回EXCEPTION_EXECUTE_HANDLER,非0时返回EXCEPTION_CONTINUE_SEARCH
这里直接禁用了UEF,那么UEF_HANDLE和UEF_C_SEARCH就都没用了。
编译后,运行程序(不要使用MSVC运行)。
结果如下图:
结论:
当异常被处理,并且返回EXCEPTION_CONTINUE_EXECUTION时,会触发VCH。
说的通俗一点:
VCH就好像是老板,而VEH和SEH、UEF等算是打工的,它们是"异常处理器",而VCH是"继续处理器"。
当异常这个烂摊子让"打工的"收拾完之后,会通知"老板",老板来做决定。
如果没人处理异常,烂摊子没人收拾,老板自然不会去"收拾烂摊子"了。
以上内容为本人分析的结果,如有错误,欢迎指出。