SEH基本模型
今天了解了一下windows的SEH机制, 顺带和其他语言的异常处理模型做了对比. 有趣的是我意识到我更擅长从几个基本公理来展开学习, 这篇文章就以这样的思路试试看. 我会给出windows SEH的几个基本公理, 随后用这几个公理来解释各类现象
__try定义一个受保护区域, 该区域代码及其调用的代码在触发异常时, 异常会被抛出. 可以理解为异常的生产者__leave可以跳出当前的__try块__except定义异常处理器, 可以理解为异常的消费者__finally定义终止处理器, 控制流离开__try块时, 无论是正常离开, unwind, return离开都会执行. 但进程被杀等极端情况除外抛出异常后首先会逐步向外搜索能够处理异常的
__except, 执行filter时处于搜索状态, 并不会真正unwind, 也不会执行__finallySEH在搜索的阶段, 会根据
__except(filter)中的filter表达式返回值做如下处理.
| 返回值 | 结果 |
|---|---|
| EXCEPTION_CONTINUE_SEARCH | 不处理, 继续向外搜索下一个__except |
| EXCEPTION_EXECUTE_HANDLER | 常规处理, 进行unwind, 进入当前__except处理 |
| EXCEPTION_CONTINUE_EXECUTION | filter已经处理异常, 不进行unwind, 回到异常指令重试 |
- 当确定了某个
__except处理异常后, 正式开始unwind, 并执行遇到的__finally块.
SEH的
__except处理是通过程序自定义的filter表达式, 而常见的异常处理模型则是通过比对错误类型是否相同来实现类似的功能.
这里用python举例. 感觉这种模型下的except ValueError就像是SEH下的__except((happen_err == ValueError) ? EXCEPTION_EXECUTE_HANDLER : EXCEPTION_CONTINUE_SEARCH).
1 | try: |
实际案例
这里是微软在结构化异常处理 (C/C++)给出的一个案例, 根据刚刚给出的几条公理来模拟.
- 在
RAISE_AN_EXCEPTION前没有疑问. 输出应该是hello\nin try\nin try\n - 在
RAISE_AN_EXCEPTION后, 根据公理5, 程序逐步向外搜索__except, 并执行其中的filter语句, 于是输出in filter\n __except返回了EXCEPTION_EXECUTE_HANDLER, 根据公理6, 执行unwind- 在unwind过程中, 根据公理7, 执行遇到的
__finally块, 输出in finally - unwind执行到
__except, 输出in except - 程序处理异常结束, 继续执行, 输出
in world
1 | puts ("hello"); |
SEH与常见的异常处理模型对比
本文一直在将SEH与其他语言异常处理进行比对, 一直忽略一个差异是两者处理的异常本身就不同
SEH 是 Windows 提供的异常分发与栈展开机制。它常处理访问违规、除零、非法指令、守护页等 CPU/Windows 异常,也可由
RaiseException产生软件异常
其他语言的异常通常是该语言运行时定义的异常对象和类型系统;它们也可能由运行时自动产生,例如Python 的
ZeroDivisionError、Java 的NullPointerException。
安全机制
在x86时代, seh的恢复信息在栈上, 可能被攻击者利用. x64时代windows把这些信息放在了只读权限的.pdata上, 可以说基本彻底封死了seh的二进制利用.
windows其他异常处理与seh关系
让ai整理了一个windows下异常处理流程图. 配合文字梳理一下:
- 极度简化的异常处理链顺序是
first-chance->VEH->SEH->UEF->second-chance->VCH, 但该链条不完全准确, 请看后文分析更详细的情况 first-chance和second-chance属于调试器决定的步骤, 调试器会根据策略决定是否声明处理完成当前异常以让程序恢复运行. 如调试器设置的断点int 3也会触发异常, 这种情况下调试器显然就会声明异常被处理完成, 不再走后续异常处理链条. 否则调试器可能不处理当前异常, 由后续异常处理机制来处理VEH是一个在ntdll.dll的函数指针链表, 其影响效力为进程全局. 无论异常发生在进程的哪个位置, 异常都可能走到此处SEH上面已经说的很详细了, 对比起VEH,VCH,UEF这种影响进程全局的异常处理机制来说,SEH的影响范围只在__try声明的范围内, 并且可以精确到线程, 是最精准的异常处理方式UEF基于SEH, 但其对进程范围生效, 并且生效范围晚于所有的SEH.(也许可以理解为一个括在main函数之外的__try?)VCH严格来说是异常恢复机制. 除了调试器之外, 无论是谁声明自己处理完成异常, 让程序恢复运行, 在恢复之前都要执行VCH. 影响范围为进程全局, 同样是挂在ntdll.dll的函数指针链表- 如果以上错误处理机制都决定不处理该异常, 调试器会受到
second-chance. 此时是异常处理链条的最后一环, 如果调试器选择不处理, 程序就会被正式终止, 选择恢复则会让程序恢复运行
特别的, UEF在附加调试器时, 只会返回
EXCEPTION_CONTINUE_SEARCH, 直接声明自己不对异常进行处理
1 | 用户态异常发生 |