🎨 颜色标记说明

  • 粉色:特别重要、主线必掌握、优先复盘的内容。
  • 蓝色:核心概念、术语、分类名。
  • 橙色:需要关注的边界、易错点、注意事项。
  • 绿色:解释类内容、帮助理解知识点。

返回:全书总览 · 上一章:继承 · 下一章:PEiD 原理

异常改变了什么

普通调用通过 CALL/RET 成对转移;异常可能从函数深处直接寻找匹配处理器,并在途中销毁已经构造的局部对象。

1
2
3
4
5
throw
-> 查找匹配处理器
-> 展开不再继续执行的栈帧
-> 调用清理与析构函数
-> 进入 catch

逆向异常代码必须同时恢复“处理器选择”和“栈展开清理”两条逻辑。

C++ throw / catch 的线索

常见线索包括:

  • 调用编译器运行库的抛出辅助函数。
  • 异常对象构造、复制或移动。
  • 与类型描述相关的数据。
  • 正常路径之外的析构清理块。
  • 函数元数据中记录的状态与处理器。

具体函数名和数据结构会随 MSVC、GCC/Clang、架构与运行库版本变化,不应只依赖单个符号名。

Windows SEH 与 C++ EH

Windows 的结构化异常处理负责传递访问冲突、除零、断点等系统异常;C++ 编译器通常在其上构建语言级异常语义。

需要区分:

  • 系统异常:由 CPU 或操作系统报告。
  • 语言异常:由 C++ throw 产生并携带类型信息。
  • 调试异常:断点、单步等也通过异常机制通知调试器。

x86 的常见形态

32 位 Windows 的传统 SEH 可通过线程环境中的异常注册链组织处理节点。编译器生成的函数可能:

  • 在入口安装异常处理记录。
  • 维护当前构造/清理状态。
  • 在退出时恢复上一处理节点。
  • 由运行库处理器解释编译器元数据。

现代编译器可能采用不同优化和安全机制,不能仅凭固定序言模板识别全部异常函数。

x64 表驱动展开

Windows x64 主要使用表驱动展开,不再依赖每个函数动态挂接传统栈链。PE 的异常目录可关联:

  • RUNTIME_FUNCTION:描述函数范围和展开信息位置。
  • UNWIND_INFO:描述序言操作、保存寄存器和栈空间。
  • UNWIND_CODE:记录如何逆转函数序言。
  • 语言处理器数据:支持 C++ catch 或 SEH 过滤等语义。

这些信息让系统在没有帧指针时仍能恢复调用现场,也是调试器回溯栈的重要依据。

展开信息描述的是如何恢复机器状态,不等于完整源码控制流。反编译器给出的 try/catch 边界仍需结合清理块和运行时行为验证。

析构与栈展开

假设函数已构造对象 A、B,构造 C 时抛出异常:

1
2
3
4
C 未完成,不按完整对象析构
B 析构
A 析构
继续向外层寻找处理器

这会产生与正常返回路径不同的析构调用。看到重复资源清理函数时,要判断它们分别服务于正常退出还是异常展开。

first chance 与 second chance

调试器第一次收到异常时称 first chance,程序自己的处理器仍有机会处理;若无人处理,调试器再次收到 second chance,继续放行通常会终止进程。

调试恶意样本或带反调试逻辑的程序时,不要把所有 first-chance 异常都当成崩溃。

分析步骤

  1. 标出所有抛出点和可能触发系统异常的指令。
  2. 查找函数的异常与展开元数据。
  3. 识别清理基本块和析构调用。
  4. 对照对象构造状态,确认哪些资源已生效。
  5. 区分过滤器、处理器和 finally/清理语义。
  6. 动态观察异常代码、处理次数与恢复地址。

学习检查清单

检查项 状态
能说明异常查找与栈展开的区别 待复盘
能解释展开过程中为什么需要调用析构函数 待复盘
能区分系统异常与 C++ 语言异常 待复盘
能说明 x64 RUNTIME_FUNCTION 与 UNWIND_INFO 的作用 待复盘
能区分 first-chance 与 second-chance 异常 待复盘