• SEH异常之编译器原理探究


    _try_except原理

    调用_except_handle3这个异常处理函数,这里并不是每个编译器的异常处理函数都是相同的,然后存入结构体,将esp的值赋给fs:[0],再就是提升堆栈的操作

    每个使用 _try _except的函数,不管其内部嵌套或反复使用多少_try _except,都只注册一遍,即只将一个 _EXCEPTION_REGISTRATION_RECORD 挂入当前线程的异常链表中(对于递归函数,每一次调用都会创建一个 _EXCEPTION_REGISTRATION_RECORD,并挂入线程的异常链表中)。

    1. typedef struct _EXCEPTION_REGISTRATION_RECORD {
    2.     struct _EXCEPTION_REGISTRATION_RECORD *Next;
    3.     PEXCEPTION_ROUTINE Handler;
    4.   } EXCEPTION_REGISTRATION_RECORD;

    可以看到只有一个异常处理函数

    那么这里编译器是如何做到只用一个异常处理函数的呢?编译器把原来_EXCEPTION_REGISTRATION_RECORD结构进行了拓展,添加了三个成员

    1. struct _EXCEPTION_REGISTRATION{
    2.         struct _EXCEPTION_REGISTRATION *prev;
    3.         void (*handler)(PEXCEPTION_RECORD, PEXCEPTION_REGISTRATION, PCONTEXT, PEXCEPTION_RECORD);
    4.         struct scopetable_entry *scopetable;
    5.         int trylevel;
    6.         int _ebp;
    7.     };       

    新堆栈结构如下

    scopetable

    1. struct scopetable_entry
    2. {
    3.        DWORD previousTryLevel  //上一个try{}结构编号 
    4.        PDWRD        lpfnFilter         //过滤函数的起始地址
    5.        PDWRD        lpfnHandler    //异常处理程序的地址     
    6. }

    查看地址可以发现有三个结构体

    存储着的正式异常函数的开始地址和结束地址

    第一个值previousTryLevel是上一个try结构的编号,这里如果在最外层就是-1,如果在第二层就是0,如果在第三层就是1,以此类推

    trylevel

    该成员表示代码运行到了哪个try结构里面,进入一个try则加1,try结构执行完成之后则减1

    _except_handler3

    1.CPU检测到异常 -> 查中断表执行处理函数 -> CommonDispatchException -> KiDispatchException -> KiUserExceptionDispatcher                 -> RtlDispatchException ->VEH -> SEH

    2.执行_except_handler3函数

    <1> 根据trylevel 选择scopetable数组

    <2> 调用scopetable数组中对应的lpfnFilter函数

    1.EXCEPTION_EXECUTE_HANDLER(1) 执行except代码

    2.EXCEPTION_CONTINUE_SEARCH(0) 寻找下一个

    3.EXCEPTION_CONTINUE_EXECUTION(-1) 重新执行

    <3> 如果lpfnFilter函数返回0 向上遍历 直到previousTryLevel=-1

    假设有两个异常点

    首先找到trylevel为0

    然后找到异常过滤表达式为1

    然后遍历数组的lpfnFilter

    如果返回值为1则调用异常处理函数,如果为0则该异常函数不处理,如果为-1则继续从原异常点向下执行

    假设在B这个地方出异常,得到trylevel为2

    那么这里就回去遍历lpfnFilter为2的地方

    假设这里返回值为0,则继续查找,注意这个地方是向上查找,首先判断当前previousTryLevel的值是否为-1,如果为-1就停止查找(-1代表已经是最外层)try结构,然后再向上找,假设这里返回值仍然为0,判断previousTryLevel的值为-1,就停止查找,没有找到响应的异常处理函数

    _try_finally原理

    无论try结构体中是什么代码,都会执行finally里面的代码

    1. // SEH6.cpp : Defines the entry point for the console application.
    2. //
    3. #include "stdafx.h"
    4. #include 
    5. VOID ExceptionTest()
    6. {
    7.  __try
    8.  {
    9.   return;
    10.   printf("Other code");
    11.  }
    12.  __finally
    13.  {
    14.   printf("Must run this code");
    15.  }
    16. }
    17. int main(int argc, char* argv[])
    18. {
    19.  ExceptionTest();
    20.  getchar();
    21.  return 0;
    22. }

    局部展开

    try里面没有异常,而是returncontinuebreak等语句时,就不会走_except_handle3这个函数,而是调用_local_unwind2进行展开

    然后调用[ebx + esi*4 + 8]

    跟进去就到了finally语句块的地方

    我们探究一下实现的原理,这里本来应该是lpfnFilter参数,指向异常处理过滤的代码的地址,但是这里是0。只要这个地方的地址为0就是finally语句块

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

    全局展开

    1. // SEH6.cpp : Defines the entry point for the console application.
    2. //
    3. #include "stdafx.h"
    4. #include <windows.h>
    5. VOID ExceptionTest()
    6. {
    7.  __try
    8.  {
    9.   __try
    10.   {
    11.    __try
    12.    {
    13.     *(int*)0 = 1;
    14.    }
    15.    __finally
    16.    {
    17.     printf("Must run this code : A");
    18.    }
    19.   }
    20.   __finally
    21.   {
    22.    printf("Must run this code : B");
    23.   }
    24.  }
    25.  __except(1)
    26.  {
    27.   printf("Here is Exception_functions");
    28.  }
    29. }
    30. int main(int argc, char* argv[])
    31. {
    32.  ExceptionTest();
    33.  getchar();
    34.  return 0;
    35. }

    全局展开就是一层一层的向上找异常处理函数,finally模块还是照常执行

    未处理异常

    入口程序的最后一道防线

    这里调用mainCRTStartup(),然后调用入口程序

    相当于这里才是一个进程开始执行的地方

    这里有一个call调用,跟进去看看

    发现有修改fs:[0]的操作,这里就相当于编译器为我们注册了一个异常处理函数

    这里到kernel32.dll里面的BaseProcessStart里面看一下,这里有一个注册SEH异常处理函数的操作

    线程启动的最后一道防线

    1. // SEH7.cpp : Defines the entry point for the console application.
    2. //
    3. #include "stdafx.h"
    4. #include <windows.h>
    5. DWORD WINAPI ThreadProc(LPVOID lpParam)
    6. {
    7.  int i = 1;
    8.  return 0;
    9. }
    10. int main(int argc, char* argv[])
    11. {
    12.  CreateThread(NULL0, ThreadProc, NULL0NULL);
    13.  getchar();
    14.  return 0;

    可以发现线程也是从kernel32.dll开始的

    然后跟进调用

    可以发现还是注册了一个异常处理函数

    还是去IDA里面看BaseThreadStart函数,发现也注册了一个SEH异常的函数

    UnhandledExceptionFilter

    相当于编译器为我们生成了一段伪代码

    1. __try
    2. {
    3. }
    4. __except(UnhandledExceptionFilter(GetExceptionInformation())
    5. {
    6.  //终止线程
    7.  //终止进程
    8. }

    只有程序被调试时,才会存在未处理异常

    UnhandledExceptionFilter的执行流程:

    1. 1) 通过NtQueryInformationProcess查询当前进程是否正在被调试,如果是,返回EXCEPTION_CONTINUE_SEARCH,此时会进入第二轮分发 
    2. 2) 如果没有被调试: 
    3. 查询是否通过SetUnhandledExceptionFilter注册处理函数 如果有就调用 
    4. 如果没有通过SetUnhandledExceptionFilter注册处理函数 弹出窗口 让用户选择终止程序还是启动即时调试器 
    5. 如果用户没有启用即时调试器,那么该函数返回EXCEPTION_EXECUTE_HANDLER

    SetUnhandledExceptionFilter

    如果没有通过SetUnhandledExceptionFilter注册异常处理函数,则程序崩溃

    测试代码如下,我自己构造一个异常处理函数callback并用SetUnhandledExceptionFilter注册,构造一个除0异常,当没有被调试的时候就会调用callback处理异常,然后继续正常运行,如果被调试则不会修复异常,因为这是最后一道防线,就会直接退出,起到反调试的效果

    1. // SEH7.cpp : Defines the entry point for the console application.
    2. //
    3. #include "stdafx.h"
    4. #include <windows.h>
    5. long _stdcall callback(_EXCEPTION_POINTERS* excp)
    6. {
    7.  excp->ContextRecord->Ecx = 1;
    8.  return EXCEPTION_CONTINUE_EXECUTION;
    9. }
    10. int main(int argc, char* argv[])
    11. {
    12.  SetUnhandledExceptionFilter(callback);
    13.  _asm
    14.  {
    15.   xor edx,edx
    16.   xor ecx,ecx
    17.   mov eax,0x10
    18.   idiv ecx
    19.  }
    20.  printf("Run again!");
    21.  getchar();
    22.  return 0;
    23. }

    直接启动可以正常运行

    使用od打开则直接退出

    KiUserExceptionDispatcher

    只有当前程序处于调试的时候才可能产生未处理异常

    1. 1) 调用RtlDispatchException  查找并执行异常处理函数
    2. 2) 如果RtlDispatchException返回真,调用ZwContinue再次进入0环,但线程再次返回3环时,会从修正后的位置开始执行。
    3. 3) 如果RtlDispatchException返回假,调用ZwRaiseException进行第二轮异常分发
    4. (参见KiUserExceptionDispatcher代码)

  • 相关阅读:
    16 | Spark SQL 的 UDF(用户自定义函数)
    时间序列预测:用电量预测 07 灰色预测算法
    【OpenGL】六、深度测试和模板测试
    软件项目管理 ——1.3.敏捷项目管理概念
    非关系数据库
    BAT034:批处理打开电脑常用功能面板
    【考研数学】六. 三重积分
    Docker 镜像的创建
    转行IC第一步:应该怎么选择公司和岗位?
    新手必看:Bitget Wallet 上购买 ETH 的步骤解析
  • 原文地址:https://blog.csdn.net/hongduilanjun/article/details/126850702