前言

windows的SEH结构化异常处理是基于线程的,传统的SEH结构化异常会基于堆栈形成一条包含异常回调函数地址的链(SEH链)。而fs:[0](TEB的第一个字段)指向这条链的链头,当有异常发生时并产送到SEH处时其会从fs:[0]开始遍历这条链。如果那个链结点的回调函数能正确处理异常则程序会返回到异常发生处,否则继续遍历一直到链尾。

顶层异常处理

一般程序开始创建任意线程运行前默认设置一个顶层异常处理程序。

  1. _try
  2. {
  3. //程序入口
  4. }
  5. _except(UnhandledExceptionFilter())
  6. {
  7. }

所谓的顶层异常处理意思是,当程序的异常通过SEH链一直传递到最后一个我们自己设置的SEH链结点都没有处理此异常时,会调用这个顶层异常处理函数。

下面对顶层异常处理进行详细分析。

  • SEH是基于线程的,因为每一个线程都有自己的TEB,又因为TEB的第一个字段指向SEH链的头部,所以每一个线程都有自己的SEH链,不同线程之间的SEH链也不同。每个线程创建之前都会安装顶层异常处理过程,而每一个顶层异常处理程序的过滤函数都是UnhandledExceptionFilter(),此函数会在内部调用全局变量kernel32!BasepCurrentTopLeveFilter保存的函数地址,此函数可以干预UnhandleExceptionFilter()的返回值。而我们可以调用SetUnhandledExceptionFilter()函数改变此全局变量的值为自定义函数的地址。所以顶层异常处理是基于进程的(顶层异常过滤函数返回值干预函数的值是全局变量保存的)。
  • 如果程序是用MSC编译器编译的程序默认的异常处理函数一般是Kernel32!_except_handlerX(),Kernel32!_except_handlerX()会先调用UnhandleExceptionFilter()过滤函数,而如果我们在UnhandleExceptionFilter()过滤函数中已经把此异常处理了则就不会执行默认的异常处理,否则默认的异常处理就是结束应用程序。

顶层异常处理在反调试中的应用

  • 为当程序产生一个异常时,首先会被系统内核捕捉然后系统内核会判断当前是否有调试器调试,有的话把异常交给调试器。如果没有调试器或者调试器处理不了此异常的话,异常会被分发给SEH链上的各个异常处理回调函数依次处理,这些回调函数的地址是通过一个链表存储。通过遍历这个链表从而调用各个异常处理回调函数。如果异常被某个回调函数正常处理了就继续从产生异常的代码处继续往下执行。如果一直遍历到链表的最后一个异常回调函数之前都没能够处理此异常的话,此异常就会交给最后一个默认的异常处理程序处理。

  • 而最后一个默认异常处理函数在调用时会先调用UnhandledExceptionFilter( )过滤函数,此函数又会调用ZwQueryInformationProcess( )函数先判断是否有调试器存在,有的话会直接返回进行异常的二次分发(一般就是会结束进程)。

  • 如果ZwQueryInformationProcess()函数没有检测到调试器的存在的话其将会执行默认异常处理。而一般默认异常处理是默认的终止程序的函数。(如果没有调试器的话异常是不会进行二次分发的,这点很重要)

  • 但是windows提供一个函数来对UnhandledExceptionFilter( )过滤函数进行干预从而修改其返回值让其正常返回,而不执行默认的异常处理终止程序。此函数就是SetUnhandledExceptionFilter( ),此函数具有唯一的参数就是设置用来干预UnhandledExceptionFilter( )过滤函数的回调函数的地址,UnhandledExceptionFilter( )会在内部调用这个函数。(一般称这个函数为顶级异常处理函数,我认为这么称是不准确的。因为真正的顶级异常处理函数是默认的异常处理函数,此函数应该称为顶级过滤干预函数)

所以一般会利用此顶层异常处理函数的特性,主动产生异常,然后调用SetUnhandledExceptionFilter()设置顶层过滤函数的干扰函数来处理异常,如果用户调式程序的话,因为顶层异常处理会检测到有调试器,因此不会调用顶层过滤干扰函数来处理异常,导致异常无法处理从而终止运行。(需要我们改变ZwQueryInformationProcess( )函数的返回值来骗过UnhandledExceptionFilter( )函数让他以为无调试器)

传统的SEH

所谓传统的SEH结构化异常处理就是没有被编译器处理过,就是最纯正的SEH结构化异常处理。通过汇编语言来编写最原始的SEH结构化异常处理.

安装SEH,其中_Handler2是此异常处理的回调函数(调用约定为_cdecl)

  1. push offset _Handler2
  2. push fs:[0]
  3. mov fs:[0],esp

卸载SEH

  1. pop fs:[0] ;恢复SEH
  2. pop eax ;平衡堆栈

以下面汇编程序为例

  1. start:
  2. assume fs:nothing ;开始安装SEH异常处理程序
  3. push offset _Handler3 ;设置异常处理回调函数的地址
  4. push fs:[0] ;把下一个EXCETION_POINTERS结构
  5. mov fs:[0],esp ;设置SEH链的头指针指向当前设置的EXCEPTION_POINTERS结构
  6. push offset _Handler2
  7. push fs:[0]
  8. mov fs:[0],esp
  9. push offset _Handler1
  10. push fs:[0]
  11. mov fs:[0],esp
  12. pop fs:[0]
  13. pop eax
  14. pop fs:[0]
  15. pop eax
  16. pop fs:[0] ;恢复SEH
  17. pop eax ;平衡堆栈
  18. invoke MessageBox,NULL,addr szDelete,NULL,MB_OK
  19. invoke ExitProcess,NULL
  20. end start

用OD查看程序,可以看到在没有执行程序前还没安装我们的SEH时,SEH链已经有数据。因为我们这个编译程序用的也是MSC编译器的ml.exe编译程序编译的,所以查看发现是程序也会安装的默认的异常处理回调函数是_except_handler4_command()。



然后我们执行程序,安装三个SEH后再查看SEH链,发现新增了3个结点。而且各个SEH链结点的异常回调函数地址为我们push的函数的地址

这就是传统的SEH链



编译器处理过的SEH

已MSC编译器为例,对于C而言编译器会在所有函数的入口点将SEH的异常处理回调函数都设置为_except_handlerX()。此函数的执行流程为:

  • 编译器会在栈上为每一个_try块设置其索引,在函数一开始会设置_try的索引为-1。_except_handlerX()函数会获取当前_try的索引,如果_try索引为-1就表示异常没有发生在_try块中,表示此函数不能处理该异常,_except_handlerX()函数返回值为ExceptionContinueSearch表示不能处理此异常。然后异常就会沿着SEH链向上传递。
  • 如果_try索引不为-1则说明异常发生在在_try块中,其再通过调用此_try对应的_except()里的过滤函数,然后根据过滤函数返回值决定是否调用_except()中的代码进行异常处理。
  • 如果过滤函数的返回值能处理异常就去调用对应_except()内部的异常处理函数,如果过滤函数的返回值不能处理次异常就继续寻找上一层_try块(父_try块)的索引。直到最后寻找到最上层_try块的上面,即默认的_try索引为-1的地方,此时表示此函数已经不能处理该异常了,_except_handlerX()函数返回值为ExceptionContinueSearch表示不能处理此异常。然后异常就会沿着SEH链向上传递。

已下面c程序为例

  1. #include <iostream>
  2. #include <Windows.h>
  3. using namespace std;
  4. int main(int argc, char* argv[])
  5. {
  6. _try
  7. {
  8. cout<<"第三层try块"<<endl;
  9. _try
  10. {
  11. cout<<"第二层try块"<<endl;
  12. _try
  13. {
  14. cout<<"第一层try块"<<endl;
  15. }
  16. _except(EXCEPTION_CONTINUE_SEARCH)
  17. {
  18. }
  19. }
  20. _except(EXCEPTION_CONTINUE_SEARCH)
  21. {
  22. }
  23. }
  24. _except(EXCEPTION_CONTINUE_SEARCH)
  25. {
  26. }
  27. return 0;
  28. }

然后在OD中查看SEH链。发现SEH链上的各个异常处理回调函数都为_except_handler4(),此异常处理回调函数起到了一个代理的作用来完成_try和_except()处理。因为程序入口点为C运行库函数,C运行库函数在调用main函数,所以SEH链上有两个异常处理函数都是_except_handler4()。

ntdll.77C9B130是默认的异常处理回调函数,和0x00CE105A(_except_handler4())一样其会在内部调用_except_handler4_command(),只不过传递的参数不同。



有一点需要注意,当异常发生时或在SEH链中传递时,先调用异常处理回调函数_except_handler4(),然后在调用各个异常过滤函数,即_except()括号中的表达式判断能不能处理异常,如果返回值能处理就调用_except()内的异常处理代码。

参考资料:《看雪加密与解密》

windows的SEH异常处理以及顶层异常处理的更多相关文章

  1. SEH:结构化异常处理 学习

    SEH:结构化异常处理 结构化异常处理机制提供了一个操作系统,用于优化结构的方案,为客户提供更强大的程序执行环境.试想一下,你写程序不用考虑内存访问错误,那里是空指针错误,一直在按照程序的逻辑结构来写 ...

  2. 反调试——Windows异常-SEH

    反调试--Windows异常-SEH 概念: SEH:Structured Exception Handling SEH是Windows默认的异常处理机制 如何使用 在代码中使用 __try​​__e ...

  3. Windows的SEH机理简要介绍

    1.异常分类 一般来说,我们把Exception分为2类,一类是CPU产生的异常,我们称之为CPU异常(或者硬件异常).另一类为是通过调用RaiseException API产生的软件异常,我们称之为 ...

  4. JAVA中的异常(异常处理流程、异常处理的缺陷)

    异常处理流程 1)首先由try{...}catch(Exception e){ System.out.println(e); e.printStackTrace(); }finally{...}结构 ...

  5. Java中的异常处理(三) - 自定义异常处理

    1.异常处理类 package second; public class MyException extends Exception { MyException (){ } MyException ( ...

  6. 异常处理过程和异常处理的执行顺序(针对try{}catch{}finally{}而言)

    1.异常的处理方式有两种分别为:try{}catch{}finally{}和throws下面简单说一下这两者的区别和联系. 2.出现异常之后如果没有进行捕获处理系统就会直接将这个异常栈的跟踪信息直接打 ...

  7. php5和php7的异常处理机制 ----thinkphp5 异常处理的分析

    1.php异常和错误 在其他语言中,异常和错误是有区别的,但是PHP,遇见自身错误时,会触发一个错误,而不是跑出异常.并且,php大部分情况,都会触发错误,终止程序执行,在php5中,try catc ...

  8. Java基础-异常处理机制 及异常处理的五个关键字:try/catch/finally/throw /throws

    笔记: /** 异常处理机制: 抓抛模型 * 1."抛", 一旦抛出,程序终止! printStackTrace()显示异常路径! * 2."抓", 抓住异常 ...

  9. JAVA高级--异常处理概念和异常处理机制

    什么是异常 程序运行的过程中发生的一些不正常事件 异常分类 Throwable Error  错误 Exception IOException         RuntimeException    ...

随机推荐

  1. linux安装mysql8.0

    linux 上安装mysql8.0 mysql版本8.0.16 MySQL Community 操作系统centos7 准备工作: mysql8.0 rpm文件 安装步骤: 1. 下载mysql的re ...

  2. Python之内存泄漏和内存溢出

    预习知识:python之MRO和垃圾回收机制 一.内存泄漏 像Java程序一样,虽然Python本身也有垃圾回收的功能,但是同样也会产生内存泄漏的问题.对于一个用 python 实现的,长期运行的后台 ...

  3. 文件锁fcntl

    一.python中的文件锁 我们在写python应用的时候,当涉及到多个进程向同一个文件write(或者read)的情况,如果几个进程同时都对这个文件进行写操作,那么文件的内容就会变得非常混乱,这个时 ...

  4. 为 .NET 打 Call,为国产平台 Gitee 打 Call,我的 .NET/C# 开源项目清单,同步维护于 Github 和 Gitee

    所有项目遵循 MIT 开源协议.可以随意使用,但是需在源代码和产品关于画面保留版权声明和我的网站链接,谢谢. Sheng.Winform.IDE Github:https://github.com/i ...

  5. CSS轮廓和圆角

    1 2 <!DOCTYPE html> 3 <html lang="en"> 4 <head> 5 <meta charset=" ...

  6. [树形DP]战略游戏

    战 略 游 戏 战略游戏 战略游戏 题目描述 Bob喜欢玩电脑游戏,特别是战略游戏.但是他经常无法找到快速玩过游戏的办法.现在他有个问题.他要建立一个古城堡,城堡中的路形成一棵树.他要在这棵树的结点上 ...

  7. Python-Tkinter 使用for循环生成列表式Button及函数调用

    Tkinter是轻量级的图形化界面,在使用中我们可能遇到需要生成一串Button按钮的情况,如图: 如果一个一个操作就太麻烦了,但我们可以通过for循环列表的形式来实现 来看看以下例子: from t ...

  8. redhat7.6 更换 centos7 YUM

    使用yum 遇到如下错误. This system is not registered to Red Hat Subscription Management. You can use subscrip ...

  9. ACCESS常见错误场景

    ACCESS常见错误场景 今天用access时发现好多报错的地方,emmm,比MySQL麻烦好多,有些甚至还要自己去配置环境 不吐槽了,进入正题: 报错场景一:您尝试执行不包含指定聚合函数的查询 第一 ...

  10. 前端DDD总结与思考

    软件开发架构演化与DDD起源 单体服务架构:大概10年前,我在武汉工作的时候,甲方客户购买我们的产品,一般都是连着设备一起购买,一套软件系统,一台惠普或者戴尔的企业级服务器,再加一个黑色的铁盒,销售部 ...