SEH异常

1.原始SEH异常介绍

  1)何为SEH异常

    所谓“SEH异常”,其本质存储在栈中的一条异常处理链表,在用户模拟异常中,ntdll!RtlDispatchException函数先尝试VEH异常处理,如果未处理成功则调用SEH异常:

    其

    

     其一个结点就是一个_EXCEPTION_REGISTRATION_RECORD数据结构:

      struct _EXCEPTION_REGISTRATION_RECORD {

        struct _EXCEPTION_REGISTRATION_RECORD* Next; //0x0

        enum _EXCEPTION_DISPOSITION (*Handler)(struct _EXCEPTION_RECORD* arg1, VOID* arg2, struct _CONTEXT* arg3, VOID* arg4);//0x4

      };

     如下是Ntdll!RtlExceptionDispatch的分析,其中涉及很多标志位,很复杂,如果有可能之后会来详细解释。

      后面的该函数处理的流程图摘自《软件调试》这本书,本来自己想画出来,但是水平太菜画的太烂。

    

  2)如何挂上SEH异常

    之前我们介绍过VEH异常,其用有关函数AddVectoredExceptionHandler可以来实现手动挂上VEH异常,但是SEH如何挂上呢?

    我们之后会介绍编译器对于SEH的扩展,但是在之前,只能用手动的汇编思路挂上SEH。

    ExceptionList在TEB+0x0位置,而我们又知道,在三环下其FS寄存器指向TEB,因此通过FS:[0]获取,然后手动构造处理结点即可。

    如下,其所构造的handler回调函数,其格式必须是固定的,这样我们就可以触发自己的SEH异常。

// 123.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"
#include <windows.h>  
#include <stdio.h>  
  
DWORD scratch;  
  
//自定义异常回调函数  
EXCEPTION_DISPOSITION __cdecl my_except_handler (  
     struct _EXCEPTION_RECORD *_ExceptionRecord,  
     void * _EstablisherFrame,  
     struct _CONTEXT *_ContextRecord,  
     void * _DispatcherContext  
    )  
{  
    printf( "Hello from my exception handler/n" );   
    
    //修复异常,使EAX寄存器指向一个有效的地址  
    _ContextRecord->Eax = (DWORD)&scratch;  
    
    //异常已修复,重新执行引发异常的指令  
    return ExceptionContinueExecution;  
}  

int main()  
{  
    DWORD handler = (DWORD)my_except_handler;  
    __asm  
    {  
        //创建_EXCEPTION_REGISTRATION_RECORD结构  
        push handler            //Handler成员  
        push fs:[0]             //Next成员,指向下一个异常帧  
        mov fs:[0],esp          //安装SEH  
    }  
    
    __asm  
    {  
        xor eax,eax                 //EAX = 0  
        mov dword ptr [eax],1234h   //写EAX指向的内存从而故意引发一个异常!  
    }  
    
    //异常已修复,此时scratch的值为0x1234  
    printf( "After writing! scratch=0x%08x/n",scratch);  
    
    __asm  
    {  
        //移除_EXCEPTION_REGISTRATION_RECORD结构  
        pop dword ptr fs:[0]  
        add esp,4  
    }  
    
    return 0;  
}  

 

2. 编译器对SEH异常拓展

  我们之前介绍过SEH异常,但是可以发现其用起来略微复杂,最起码需要用到汇编知识,并且也不太直观。

  为解决这个问题,Windows的编译器提供了一种_try_except_finally语法,其可以有效的进行处理。

   

  1)异常过滤表达式

    理解SEH异常的一个重要就是SEH的过滤表达式,其存在如下三个值:

    // Defined values for the exception filter expression
    #define EXCEPTION_EXECUTE_HANDLER      1 // 执行handler函数
    #define EXCEPTION_CONTINUE_SEARCH      0 // 未能识别异常,继续调用SEH链往下
    #define EXCEPTION_CONTINUE_EXECUTION (-1) // 执行完毕,继续按照 CONTEXT.Eip中的值来运行

    即在过滤表达式中,你有一次机会对异常进行处理,判断处理结果如何再做下一步操作,注意要区分一般的SEH异常。

    一般的SEH异常返回的是  _EXCEPTION_DISPOSITION,其定义如下:

    typedef enum _EXCEPTION_DISPOSITION
    {
        ExceptionContinueExecution,
        ExceptionContinueSearch,
        ExceptionNestedException,
        ExceptionCollidedUnwind
    } EXCEPTION_DISPOSITION;

    初学者很容易把这两个搞混(我开始被搞混了很久),之所以不理解,是因为没有深刻理解SEH拓展的汇编代码,之后我们会来详细进行分析。

  2)拓展SEH样例代码:

// 123.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"
#include <windows.h>  
#include <stdio.h>  
  
DWORD scratch;  
int b; 
int filter(PEXCEPTION_POINTERS p,int ecode){
    printf("启动过滤表达函数\r\n");
    b=1;
    return EXCEPTION_CONTINUE_SEARCH;
}

int main()  
{  
    _try{
        b = 0;
        int a = 1 / b;
        printf("异常处理完成");
    }
    _except(filter(GetExceptionInformation(),GetExceptionCode())){
        printf("handler处理程序");
    }

    return 0;  
}  

 

3. 拓展SEH在栈中的数据结构的变化

  之前的SEH结构结构如下:

  struct _EXCEPTION_REGISTRATION_RECORD {

    struct _EXCEPTION_REGISTRATION_RECORD* Next; //0x0

    enum _EXCEPTION_DISPOSITION (*Handler)(struct _EXCEPTION_RECORD* arg1, VOID* arg2, struct _CONTEXT* arg3, VOID* arg4);//0x4

  };

   而现在的结构如下

  struct _EXCEPTION_REGISTRATION{

    struct _EXCEPTION_REGISTRATION_RECORD* Next; //0x0

    enum _EXCEPTION_DISPOSITION (*Handler)(struct _EXCEPTION_RECORD* arg1, VOID* arg2, struct _CONTEXT* arg3, VOID* arg4);   //0x4

    struct scopetable_entry * scopetable

    int trylevel;

    ine _ebp;

  };

  我们根据这个结构来查看反汇编代码:

  可以发现,其编译器先处理SEH异常结构,再来提升堆栈。

  另外,值得注意的是:Release版本会进行大量优化,但当出现_try{}_except(){},其不会对其进行优化,因为要保证堆栈结构。

  

   要明白,其是拓展结构,并没有影响原来的结构,原来的结构在这里依然可以使用的,故其SEH拓展后的结构如下所示:

   

 

4._EXCEPTION_REGISTRATION 结构中的实现细节

  我们之前介绍过,无论一个函数中有多少个Try,其只要一个_EXCEPTION_REGISTRATION结构体就好。

  但是,我们肯定很好奇,其是如何实现的。下面,我们就来分析一下其是如何来实现的。

  1)ScopeTable表结构

    其是一串结构体数组,理解它的含义是理解SEH拓展的关键,结构体如下:

      previousTryLevel – 上一个try的索引

      lpfnFilter – except过滤表达式位置

      lpfnHandler – except_handler执行函数

  2)我们现在分析一层复杂的ScopeTable结构:

    如下图,很明显,第一个previousTryLevel表示的是其存在上一层的嵌套,现在我们有一个问题,try0先执行还是try1先执行?

    当然是try1先执行,然后沿着previousTryLevel找到try0的except,明白了这个逻辑再来看这张图就很好理解。

    lpfnFilter指向其过滤表达式,lpfnHanler指向_except_handler,异常处理代码。

    

  3)trylevel的含义

    我们看其反汇编代码,当做的第一件事就是往trylevel中填写一个数字,我们在ScopeTable中看到其存在一个编号。

    因此,很容易推断出 trylevel记录当前try所在的编号。

    通过trylevel这个编号,进入表通过 (Scopetable+0x0c*trylevel) 计算,就很容易找到各个元素。

    

 

5. 拓展SEH的进一步理解(EXCEPTION_REGISTRATION的形成与局部展开)

  如果从上面一路看下来,你一定会理解比较深刻,这里有几个误区一定要明确:

  ① 对于拓展的SEH异常,不要将except_handler与handler划等号,拓展SEH异常统一写入一个handler函数(我们之后会对此来进行逆向分析)。

  ② 一个函数只会产生一个结点EXCEPTION_REGISTRATION,并不是多少个try会产生多少个结点,因为通过这个扩展以及ScopeTable表一个足够;

    至于如何正确找到执行,这就涉及全局展开与局部展开,也是我们下面要分析的重点。

  1)拓展SEH的形成 – __SEH_prolog函数分析

    前面已经说明过拓展SEH的结构,但是其是如何形成的呢?

    编译器一般是手动构造的,我们之前分析反汇编代码就是,但是在Windows函数中存在一个 __SEH_prolog函数,通过分析它我们就可以很清楚地了解构造了。

    

    看下面反汇编代码解析,就能了解其详细的构成经过:

    

    2)局部展开 _local_unwind

      所谓局部展开,就是当遇见goto,break,return 等跳出try_finally_域时,调用其当前所嵌套的_finally域;

      如下,即使在try中return,其也会在finally中执行,这就是finally的意义,因此其需要展开上面所有嵌套的

     

      ScopeTable表的其存在finally与except两种元素,其图表如下:

      

        现在我们分析局部展开函数_local_unwind2,其思路比较明确,其核心思路就是判断Filter == 0 就执行函数,执行完返回到上一层。

      其中一个_NLG_Return2()函数,其作用就是方便return返回使用,这样嵌套的思路就很明确。

      

 

6. 拓展SEH的进一步理解(全局展开)  现在分析全局展开,因为全局展开涉及异常链表的查询,比较复杂,因此单独拿出来讲解。

  1)一个奇怪的调用方式

    如下,按正常逻辑,其应该按照finally、filter1、filter2、handler依次执行,但是实际情况却是filter先开始循环执行。

    其实,所谓全局展开,就是当出现异常时,寻找到正确的except_hander的处理方式。

    

  2)except_handler3函数分析

    我们在SEH异常中统一填入except_hander3函数,下面我们先来分析一下这个函数,如下图,之后

    

   3)RtlUnwind函数分析

    __global_unwind2函数最终会调用一个RtlUnwind函数,该函数内容比较杂乱,其大体流程如下

    

 

版权声明:本文为onetrainee原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://www.cnblogs.com/onetrainee/p/12795097.html