VEH,VCH,UEF Windows向量化异常处理机制详解

先声明一下,本文重点是讲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(异常处理程序)的调用顺序:

先来个链接

看雪:【原创】白话windows之四 异常处理机制

按照上面帖子的说法。这些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;
}

      
      
     
     
    
    
   
   

以上代码执行时,会产生2次异常,由以下代码引发:
  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是"继续处理器"。

当异常这个烂摊子让"打工的"收拾完之后,会通知"老板",老板来做决定。

如果没人处理异常,烂摊子没人收拾,老板自然不会去"收拾烂摊子"了。


以上内容为本人分析的结果,如有错误,欢迎指出。

  • 2
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
以下是一个使用VEH机制来拦截并接管MessageBox函数的异常的示例代码: ```c++ #include <Windows.h> // Vectored Exception Handler函数 LONG WINAPI VehHandler(PEXCEPTION_POINTERS pExceptionInfo) { // 判断异常类型为访问冲突异常 if (pExceptionInfo->ExceptionRecord->ExceptionCode == EXCEPTION_ACCESS_VIOLATION) { // 恢复异常现场,使程序继续执行 pExceptionInfo->ContextRecord->Eip += 2; // 跳过引发异常的指令,使其继续执行 // 弹出自定义的消息框 MessageBox(NULL, "Exception occurred!", "Custom Exception", MB_OK | MB_ICONERROR); // 返回处理结果,终止异常处理链 return EXCEPTION_EXECUTE_HANDLER; } // 返回继续搜索异常处理链 return EXCEPTION_CONTINUE_SEARCH; } int main() { // 注册Vectored Exception Handler PVOID pHandler = AddVectoredExceptionHandler(1, VehHandler); if (pHandler == NULL) { // 处理注册失败情况 // ... return 1; } // 触发访问冲突异常 int* p = nullptr; *p = 42; // 移除Vectored Exception Handler RemoveVectoredExceptionHandler(pHandler); return 0; } ``` 在上述代码中,我们定义了一个VEH处理函数`VehHandler`,它会在异常发生时被调用。在`VehHandler`中,我们首先判断异常类型是否为访问冲突异常(EXCEPTION_ACCESS_VIOLATION),如果是,则进行自定义的异常处理逻辑。在本例中,我们跳过引发异常的指令(使程序继续执行),然后弹出一个自定义的消息框来通知异常发生。最后,我们返回`EXCEPTION_EXECUTE_HANDLER`来终止异常处理链。 在`main`函数中,我们首先注册VEH处理函数,然后触发一个访问冲突异常(通过对空指针进行解引用)。当异常发生时,VEH处理函数将被调用,执行我们定义的异常处理逻辑。最后,我们移除VEH处理函数并结束程序。 请注意,上述代码只是一个简单示例,实际使用VEH机制需要根据具体需求进行详细的异常处理和错误处理

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值