• SEH异常


    Windows内核分析索引目录:https://www.cnblogs.com/onetrainee/p/11675224.html

    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("启动过滤表达函数
    ");
        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函数,该函数内容比较杂乱,其大体流程如下

        

  • 相关阅读:
    Cesium入门-2-增加地形
    Cesium中常用的一些地理数据文件 以及数据相关的东西
    飞行姿态角度表示: heading pitch roll
    Cesium入门-3-官方完整实例
    Cesium中级教程6
    Postgresql添加/删除触发器示例
    VUE课程---6、v-text和v-html指令
    VUE课程---5、vue devtools
    VUE课程---4、MVVM原理
    js简单对象(plain javascript object)
  • 原文地址:https://www.cnblogs.com/onetrainee/p/12795097.html
Copyright © 2020-2023  润新知