C和C++安全编码(原书第2版)
上QQ阅读APP看书,第一时间看更新

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]。

[1] Microsoft Visual Studio.NET编译器支持创建带有SAFE SEH的代码,但这种检查只在WindowsXP SP2中强制执行。