SEH基本模型

今天了解了一下windows的SEH机制, 顺带和其他语言的异常处理模型做了对比. 有趣的是我意识到我更擅长从几个基本公理来展开学习, 这篇文章就以这样的思路试试看. 我会给出windows SEH的几个基本定理, 随后用这几个定理来解释各类现象

  1. __try定义一个受保护区域, 该区域代码及其调用的代码在触发异常时, 异常会被抛出. 可以理解为异常的生产者

  2. __leave可以跳出当前的__try

  3. __except定义异常处理器, 可以理解为异常的消费者

  4. __finally定义终止处理器, 控制流离开__try块时, 无论是正常离开, unwind, return离开都会执行. 但进程被杀等极端情况除外

  5. 抛出异常后首先会逐步向外搜索能够处理异常的__except, 执行filter时处于搜索状态, 并不会真正unwind, 也不会执行__finally

  6. SEH在搜索的阶段, 会根据__except(filter)中的filter表达式返回值做如下处理.

返回值 结果
EXCEPTION_CONTINUE_SEARCH 不处理, 继续向外搜索下一个__except
EXCEPTION_EXECUTE_HANDLER 常规处理, 进行unwind, 进入当前__except处理
EXCEPTION_CONTINUE_EXECUTION filter已经处理异常, 不进行unwind, 回到异常指令重试
  1. 当确定了某个__except处理异常后, 正式开始unwind, 并执行遇到的__finally块.

SEH的__except处理是通过程序自定义的filter表达式, 而常见的异常处理模型则是通过比对错误类型是否相同来实现类似的功能.

这里用python举例. 感觉这种模型下的except ValueError就像是SEH下的__except((happen_err == ValueError) ? EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH).

1
2
3
4
5
6
7
8
try:
work()
except ValueError:
recover(0)
except TypeError:
recover(1)
finally:
cleanup()

实际案例

这里是微软在结构化异常处理 (C/C++)给出的一个案例, 根据刚刚给出的几条定理来模拟.

  1. RAISE_AN_EXCEPTION前没有疑问. 输出应该是hello\n in try\n in try\n
  2. RAISE_AN_EXCEPTION后, 根据公理5, 程序逐步向外搜索__except, 并执行其中的filter语句, 于是输出in filter\n
  3. __except返回了EXCEPTION_EXECUTE_HANDLER, 根据公理6, 执行unwind
  4. 在unwind过程中, 根据公理7, 执行遇到的__finally块, 输出in finally
  5. unwind执行到__except, 输出in except
  6. 程序处理异常结束, 继续执行, 输出in world
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
puts ("hello");
__try
{
puts ("in try");
__try
{
puts ("in try");
RAISE_AN_EXCEPTION ();
}
__finally
{
puts ("in finally");
}
}
__except (puts ("in filter"), EXCEPTION_EXECUTE_HANDLER)
{
puts ("in except");
}
puts ("world");

SEH与常见的异常处理模型对比

本文一直在将SEH与其他语言异常处理进行比对, 一直忽略一个差异是两者处理的异常本身就不同

SEH 是 Windows 提供的异常分发与栈展开机制。它常处理访问违规、除零、非法指令、守护页等 CPU/Windows 异常,也可由RaiseException 产生软件异常。其他语言的异常通常是该语言运行时定义的异常对象和类型系统;它们也可能由运行时自动产生,例如Python 的 ZeroDivisionError、Java 的 NullPointerException

安全机制

在x86时代, seh的恢复信息在栈上, 可能被攻击者利用. x64时代windows把这些信息放在了只读权限的.pdata上, 可以说基本彻底封死了seh的二进制利用.