avatar

Catalog
编译器扩展SEH

前言

前面学习了VEH以及SEH,相较于全局链表的VEH,局部链表SEH有一个特别不方便的一点是,需要手动构造SEH结构体,并手动将其挂入SEH链表中,过程较为繁琐。本篇学习的编译器扩展SEH,就是在SEH的基础上,通过编译器的支持,简化了构造SEH结构体以及将其挂入链表的步骤

需要注意一点,SEH以及VEH机制都是Windows系统下的异常处理机制,其它操作系统对于异常的处理方式并不一定相同,因此这里谈到的SEH扩展,主要是Windows平台相关编程语言主打的编译器对SEH功能的适配和优化

编译器对SEH的支持

首先来回顾一下,原先挂一个SEH到链表上的步骤是怎样的。

再来看看编译器扩展SEH后是如何实现的,以Visual C++ 6.0为例(本篇均以此编译器为例)

可以看到,简化了非常多,关键字try形成了一个区块,只需要将可能出现异常的代码放入这个try块里,编译器会替我们将异常处理程序嵌入到SEH结构体中,并将其挂入局部SEH链表内

过滤表达式

except使用过滤表达式来判断当前异常处理程序是否可以处理该异常

取值

except里的过滤表达式,只能是以下3个值中的一个:

  • EXCEPTION_EXECUTE_HANDLER(1):执行except中的异常处理代码 。
  • EXCEPTION_CONTINUE_SEARCH(0):寻找下一个异常处理函数。
  • EXCEPTION_CONTINUE_EXECUTION(-1):返回至异常错误处并重新执行。

形式

过滤表达式有3种写法:

  • 直接写常量值:这个比较好理解,就是填过滤表达式取值中的一个。

  • 表达式

    c
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    _try
    {
    _asm
    {
    xor edx,edx
    xor ecx,ecx
    mov eax,10
    idiv ecx //EDX = (EAX/ECX)取余
    }
    }

    //如果异常码为0xC0000094返回1否则返回0
    _except(GetExceptionCode() == 0xC0000094 ? EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH)
    {
    printf("如果出现异常 此处处理\n");
    }

    表达式的写法相较于常量值,就是换了一种形式,它并没有做处理,但是多了逻辑判断。

  • 调用函数写法

    c
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    //参数根据需要来写,可以不要参数
    int ExceptFilter(LPEXCEPTION_POINTERS pExceptionInfo)
    {
    pExceptionInfo->ContextRecord->Ecx = 1; //异常处理
    return EXCEPTION_CONTINUE_EXECUTION; //返回出错位置重新执行
    }

    int main()
    {
    _try
    {
    _asm
    {
    xor edx,edx
    xor ecx,ecx
    mov eax,10
    idiv ecx //EDX = (EAX/ECX)取余
    }
    }

    //GetExceptionInformation获取异常结构指针
    _except(ExceptFilter(GetExceptionInformation()))
    {
    printf("如果出现异常 此处处理\n");
    }

    getchar();
    }

    调用函数的方式,和前一篇手动挂入SEH链表处理异常的方式比较类似,过滤表达式的值其实就是异常处理函数的返回值,因此采用调用函数的方式,就相当于编写SEH的handler。那么问题来了,在采用调用函数的写法下,已经有异常处理函数了,那么except内部的处理会执行吗

    这要取决于异常处理函数的返回值了,也就是过滤表达式的取值。以上面的代码为例,由于返回值是EXCEPTION_CONTINUE_EXECUTION,所以异常处理函数执行完后,会回到异常处执行,这时由于修改了Ecx的值,因此再次执行时不会发生异常,也就不会再跳转到except处执行了。

    那么问题又来了,既然异常处理函数已经实现了功能,为什么还要有except区块呢?

    其实这里的关键在于GetExceptionInformation函数,调用它可以获取到异常结构的指针,从而获取到异常发生时记录的信息以及上下文环境,若没有异常处理函数,那么在except函数内部也可以调用GetExceptionInformation获取到相关的参数,从而对异常进行处理。同样,如果异常处理函数的返回值为EXCEPTION_CONTINUE_HANDLER,则会在异常函数处理完之后,跳转到except块中执行。大部分情况下,使用except块进行处理异常是足够的,它可以让3环程序员打印出必要的信息,起到调试的作用,并不会像异常处理函数那样,修改真正的寄存器的值,使得程序重新执行。也因此,大部分表达式的值,最终都是1,这样就直接进入except执行。

参考资料

参考书籍:

  • 《软件调试 卷2:Windows平台调试》p249~p259 —— 张银奎

参考教程:

  • 海哥逆向中级班预习班

参考链接:

Author: cataLoc
Link: http://cata1oc.github.io/2020/08/24/%E7%BC%96%E8%AF%91%E5%99%A8%E6%89%A9%E5%B1%95SEH/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.
Donate
  • 微信
    微信
  • 支付寶
    支付寶