SafeSEH的原理:在程序调用异常处理函数前,对要调用的异常处理函数进行一系列的有效性校验,当发现异常处理函数不可靠时将终止异常处理函数的调用。
safeSEH的实现需要操作系统和编译器的双重支持,二者缺一都会降低SafeSEH的保护能力。
1、编译器所做工作
首先看下编译器在SafeSEH机制中所做的工作。通过启用/SafeSEH连接选项可以让编译好的程序具备SafeSEH功能,这一链接选项在VS2003之后默认开启。
启用该链接选项之后,编译器在编译程序的时候将程序所有的异常处理函数地址提取出来,编入一张安全SEH表,并将这张表放在程序的映像里面。当程序调用异常处理函数的时候,会将函数地址与安全SEH表进行匹配,检查调用的异常处理函数是否位于安全SEH表中。
下图是同一段代码在VC++6.0和VS2008分别编译后安全SEH表的区别:
注:具有SafeSEH编译能力的VS 2008在编译程序时将程序中的异常处理函数的地址提取出来放到安全S.E.H表中。
2、操作系统所做工作
(1)检查异常处理链是否位于当前程序的栈中。如果不在当前栈中,程序将终止异常处理函数的调用;
(2)检查异常处理函数指针是否指向当前程序的栈中。如果指向当前栈中,程序将会终止异常处理函数的调用;
(3)在前面两项检查都通过后,程序将会调用一个全新的函数RtlIsValidHandler(),来对异常处理函数的有效性进行验证。
3、关于函数RtlIsValidHandler()流程
该函数判断异常处理函数是不是在加载模块的内存空间,如果属于加载模块的内存空间,校验函数将依次进行如下校验:
(1)判断程序是否设置了IMAGE_DLLCHARACTERISTICS_NO_SET标识。如果设置了这个标识,这个程序内的异常将会被忽略。所以当这个标志被设置时,函数直接返回校验失败;
(2)检测程序是否包含安全SEH表。如果程序包含安全SEH表,则将当前的异常处理函数地址与该表进行匹配,匹配成功则返回校验成功,匹配失败则返回校验失败;
(3)判断程序是否设置ILonly标识。如果设置了这个标识,说明该程序只包含.NET编译人中间语言,函数直接返回校验失败;
(4)判断异常处理函数地址是否位于不可执行页上。当异常处理函数地址位于不可执行页上时,校验函数将检测DEP是否开启,如果系统未开启DEP则返回校验成功,否则程序抛出访问违例的异常;
如果异常处理函数的地址没有包含在加载模块的内存空间,校验函数将直接进行DEP相关检测,函数依次进行如下校验:
(1)判断异常处理函数是否位于不可执行页上。当异常处理函数位于不可执行页上时,校验函数将检测DEP是否开启,如果系统未开启DEP则返回校验成功,否则程序抛出访问违例的异常;
(2)判断系统是否允许跳转到加载模块的内存空间外执行,如果允许则返回校验成功,否则返回校验失败。
RtlIsValidHandler()函数的校验流程如下图所示:
S.E.H终极特权——如果S.E.H中的异常函数指针指向堆区,即使安全校验发现了S.E.H可能不可信,仍然会调用其已经被修改过的异常处理函数。因此只要将shellcode布置到堆区就可以直接跳转执行。
注:这节所有的有关绕过safeS.E.H机制的讨论都不考虑DEP的影响。