SEH异常之编译器原理探究
字数 1965 2025-08-07 00:34:54

SEH异常处理机制深入解析:编译器实现原理

0x00 前言

本文深入探究Windows结构化异常处理(SEH)中编译器对_try_except_try_finally的实现原理,涵盖异常处理流程、数据结构、编译器扩展机制等核心内容。

0x01 _try_except原理

基本机制

  1. 编译器调用_except_handle3作为异常处理函数(不同编译器可能不同)
  2. 将ESP值赋给FS:[0],提升堆栈
  3. 每个使用_try_except的函数只注册一个_EXCEPTION_REGISTRATION_RECORD结构到线程异常链表中

关键数据结构

原始结构

typedef struct _EXCEPTION_REGISTRATION_RECORD {
    struct _EXCEPTION_REGISTRATION_RECORD *Next;
    PEXCEPTION_ROUTINE Handler;
} EXCEPTION_REGISTRATION_RECORD;

编译器扩展结构

struct _EXCEPTION_REGISTRATION {
    struct _EXCEPTION_REGISTRATION *prev;
    void (*handler)(PEXCEPTION_RECORD, PEXCEPTION_REGISTRATION, PCONTEXT, PEXCEPTION_RECORD);
    struct scopetable_entry *scopetable;
    int trylevel;
    int _ebp;
};

scopetable结构

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

关键成员说明

  1. previousTryLevel

    • 最外层为-1
    • 第二层为0
    • 第三层为1
    • 以此类推
  2. trylevel

    • 表示代码运行到哪个try结构中
    • 进入try结构时加1
    • try结构执行完成后减1

异常处理流程

  1. CPU检测到异常 → 查中断表执行处理函数 → CommonDispatchException → KiDispatchException → KiUserExceptionDispatcher → RtlDispatchException → VEH → SEH
  2. 执行_except_handler3函数:
    • 根据trylevel选择scopetable数组
    • 调用scopetable数组中对应的lpfnFilter函数
      • EXCEPTION_EXECUTE_HANDLER(1):执行except代码
      • EXCEPTION_CONTINUE_SEARCH(0):寻找下一个处理程序
      • EXCEPTION_CONTINUE_EXECUTION(-1):重新执行
    • 如果lpfnFilter返回0,向上遍历直到previousTryLevel=-1

处理示例

假设有两个异常点:

  1. 首先找到trylevel为0
  2. 找到异常过滤表达式为1
  3. 遍历数组的lpfnFilter:
    • 返回1:调用异常处理函数
    • 返回0:该异常函数不处理
    • 返回-1:从原异常点继续执行

0x02 _try_finally原理

基本特性

无论try结构体中是什么代码(包括return、continue、break等),都会执行finally代码块。

局部展开

当try中没有异常而是控制流改变语句时:

  1. 不调用_except_handle3
  2. 调用_local_unwind2进行展开
  3. 调用[ebx + esi*4 + 8]进入finally语句块

识别特征:lpfnFilter参数为0时表示finally语句块

全局展开

多层嵌套时的处理:

  1. 一层层向上查找异常处理函数
  2. finally模块照常执行

0x03 未处理异常机制

入口程序的最后防线

  1. mainCRTStartup()是进程开始执行的地方
  2. 内部修改FS:[0],注册异常处理函数
  3. kernel32.dll中的BaseProcessStart注册SEH异常处理函数

线程的最后防线

  1. 线程从kernel32.dllBaseThreadStart开始
  2. 同样注册了SEH异常处理函数

UnhandledExceptionFilter流程

相当于编译器生成的伪代码:

__try{}__except(UnhandledExceptionFilter(GetExceptionInformation())) {
    // 终止线程或进程
}

执行流程:

  1. 通过NtQueryInformationProcess查询进程是否被调试
    • 如果是,返回EXCEPTION_CONTINUE_SEARCH,进入第二轮分发
  2. 如果没有被调试:
    • 检查是否通过SetUnhandledExceptionFilter注册处理函数
      • 有:调用注册函数
      • 无:弹出窗口让用户选择终止程序或启动即时调试器
    • 用户未启用即时调试器时返回EXCEPTION_EXECUTE_HANDLER

SetUnhandledExceptionFilter应用

可用于实现反调试:

long __stdcall callback(_EXCEPTION_POINTERS* excp) {
    excp->ContextRecord->Ecx = 1;  // 修复异常
    return EXCEPTION_CONTINUE_EXECUTION;
}

int main() {
    SetUnhandledExceptionFilter(callback);
    _asm {
        xor edx, edx
        xor ecx, ecx
        mov eax, 0x10
        idiv ecx  // 除0异常
    }
    printf("Run again!");
    return 0;
}

特性:

  • 直接运行:异常被修复,程序继续
  • 调试运行:直接退出

KiUserExceptionDispatcher

仅当程序被调试时才可能产生未处理异常:

  1. 调用RtlDispatchException查找并执行异常处理函数
    • 返回真:调用ZwContinue再次进入0环,返回3环时从修正位置执行
    • 返回假:调用ZwRaiseException进行第二轮异常分发

总结

本文详细解析了Windows SEH机制的编译器实现,包括:

  1. _try_except的扩展结构和处理流程
  2. _try_finally的局部和全局展开机制
  3. 未处理异常的最后防线及反调试应用
  4. 关键数据结构和系统调用流程

理解这些底层原理对于开发健壮的异常处理系统和实现安全防护机制至关重要。

SEH异常处理机制深入解析:编译器实现原理 0x00 前言 本文深入探究Windows结构化异常处理(SEH)中编译器对 _try_except 和 _try_finally 的实现原理,涵盖异常处理流程、数据结构、编译器扩展机制等核心内容。 0x01 _ try_ except原理 基本机制 编译器调用 _except_handle3 作为异常处理函数(不同编译器可能不同) 将ESP值赋给FS:[ 0 ],提升堆栈 每个使用 _try_except 的函数只注册一个 _EXCEPTION_REGISTRATION_RECORD 结构到线程异常链表中 关键数据结构 原始结构 编译器扩展结构 scopetable结构 关键成员说明 previousTryLevel : 最外层为-1 第二层为0 第三层为1 以此类推 trylevel : 表示代码运行到哪个try结构中 进入try结构时加1 try结构执行完成后减1 异常处理流程 CPU检测到异常 → 查中断表执行处理函数 → CommonDispatchException → KiDispatchException → KiUserExceptionDispatcher → RtlDispatchException → VEH → SEH 执行 _except_handler3 函数: 根据trylevel选择scopetable数组 调用scopetable数组中对应的lpfnFilter函数 EXCEPTION_ EXECUTE_ HANDLER(1):执行except代码 EXCEPTION_ CONTINUE_ SEARCH(0):寻找下一个处理程序 EXCEPTION_ CONTINUE_ EXECUTION(-1):重新执行 如果lpfnFilter返回0,向上遍历直到previousTryLevel=-1 处理示例 假设有两个异常点: 首先找到trylevel为0 找到异常过滤表达式为1 遍历数组的lpfnFilter: 返回1:调用异常处理函数 返回0:该异常函数不处理 返回-1:从原异常点继续执行 0x02 _ try_ finally原理 基本特性 无论try结构体中是什么代码(包括return、continue、break等),都会执行finally代码块。 局部展开 当try中没有异常而是控制流改变语句时: 不调用 _except_handle3 调用 _local_unwind2 进行展开 调用 [ebx + esi*4 + 8] 进入finally语句块 识别特征 :lpfnFilter参数为0时表示finally语句块 全局展开 多层嵌套时的处理: 一层层向上查找异常处理函数 finally模块照常执行 0x03 未处理异常机制 入口程序的最后防线 mainCRTStartup() 是进程开始执行的地方 内部修改FS:[ 0 ],注册异常处理函数 kernel32.dll 中的 BaseProcessStart 注册SEH异常处理函数 线程的最后防线 线程从 kernel32.dll 的 BaseThreadStart 开始 同样注册了SEH异常处理函数 UnhandledExceptionFilter流程 相当于编译器生成的伪代码: 执行流程: 通过 NtQueryInformationProcess 查询进程是否被调试 如果是,返回 EXCEPTION_CONTINUE_SEARCH ,进入第二轮分发 如果没有被调试: 检查是否通过 SetUnhandledExceptionFilter 注册处理函数 有:调用注册函数 无:弹出窗口让用户选择终止程序或启动即时调试器 用户未启用即时调试器时返回 EXCEPTION_EXECUTE_HANDLER SetUnhandledExceptionFilter应用 可用于实现反调试: 特性: 直接运行:异常被修复,程序继续 调试运行:直接退出 KiUserExceptionDispatcher 仅当程序被调试时才可能产生未处理异常: 调用 RtlDispatchException 查找并执行异常处理函数 返回真:调用 ZwContinue 再次进入0环,返回3环时从修正位置执行 返回假:调用 ZwRaiseException 进行第二轮异常分发 总结 本文详细解析了Windows SEH机制的编译器实现,包括: _try_except 的扩展结构和处理流程 _try_finally 的局部和全局展开机制 未处理异常的最后防线及反调试应用 关键数据结构和系统调用流程 理解这些底层原理对于开发健壮的异常处理系统和实现安全防护机制至关重要。