
3.10.1 结构化异常处理
SEH通常在编译器级别通过try...catch语句实现,如例3.16所示。
例3.16 try...catch语句
1 try { 2 // 在这里做一些事 3 } 4 catch(...){ 5 // 在这里处理异常 6 } 7 __finally { 8 // 在这里处理清理 9 }
try块中引发的任何异常都将被匹配的catch块处理。如果catch块无法处理该异常,那么它将被传回之前的范围块。__finally关键字是微软对C/C++语言的扩展,用于表示一个代码块,该代码块被调用来清理由try块说明的任何东西。不管try块如何退出,该关键字都被调用。
对结构化异常处理而言,Windows为每线程的异常处理程序提供了特殊支持。编译器产生的代码将一个指向EXCEPTION_REGISTRATION结构的指针的地址,写入fs段寄存器所引用的地址。这个结构在Visual C++运行库源文件的EXSUPP.INC中使用汇编语言struc定义,它包含2个数据元素,如例3.17所示。
例3.17 EXCEPTION_REGISTRATION struc的定义
1 _EXCEPTION_REGISTRATION struc 2 prev dd ? 3 handler dd ? 4 _EXCEPTION_REGISTRATION ends
在这个结构中,prev是一个指向异常处理程序链中前一个EXCEPTION_HANDLER结构的指针,handler则是一个指向实际异常处理程序函数的指针。
Windows针对异常处理程序施加了几条强制性的措施,以确保异常处理程序链和系统的完整性:
1.EXCEPTION_REGISTRATION结构必须在栈上分配。
2.prev EXCEPTION_REGISTRATION结构必须处于栈中较高的地址。
3.EXCEPTION_REGISTRATION结构必须按双字边界对齐。
4.如果可执行映像头部列出了SAFE SEH处理程序地址 [1],那么处理器地址必须被列为SAFE SEH处理程序。反之,任何结构化异常处理程序都可以被调用。
编译器在函数开头(function prolog)初始化栈帧。例3.18展示了Visual C++产生的典型的函数开头。这段代码建立了如表3.1所示的栈帧结构。编译器在栈中为局部变量保存空间。因为异常处理程序地址紧跟在局部变量之后,因此,如果一个栈变量发生缓冲区溢出,那么异常处理程序地址就可以被覆写为任意值。
例3.18 栈帧初始化
1 push ebp 2 mov ebp, esp 3 and esp, 0FFFFFFF8h 4 push 0FFFFFFFFh 5 push ptr [Exception_Handler] 6 mov eax, dword ptr fs:[00000000h] 7 push eax 8 mov dword ptr fs:[0], esp
表3.1 具有异常处理程序的栈帧
除了覆写单独的函数指针外,还可以替换线程环境块(Thread Environment Block,TEB)中的指针,已注册的异常处理程序的列表就是由该指针所引用的。攻击者需要仿造一个列表入口作为攻击代码的一部分,然后利用任意内存写技术修改第一个异常处理程序域。尽管最新版本的Windows已经加入了列表入口有效性校验功能,然而Litchfield示范了在很多情况下都可以成功地进行利用攻击[Litchfield 2003a]。