前言
windows的SEH结构化异常处理是基于线程的,传统的SEH结构化异常会基于堆栈形成一条包含异常回调函数地址的链(SEH链)。而fs:[0](TEB的第一个字段)指向这条链的链头,当有异常发生时并产送到SEH处时其会从fs:[0]开始遍历这条链。如果那个链结点的回调函数能正确处理异常则程序会返回到异常发生处,否则继续遍历一直到链尾。
顶层异常处理
一般程序开始创建任意线程运行前默认设置一个顶层异常处理程序。
_try
{
//程序入口
}
_except(UnhandledExceptionFilter())
{
}
所谓的顶层异常处理意思是,当程序的异常通过SEH链一直传递到最后一个我们自己设置的SEH链结点都没有处理此异常时,会调用这个顶层异常处理函数。
下面对顶层异常处理进行详细分析。
- SEH是基于线程的,因为每一个线程都有自己的TEB,又因为TEB的第一个字段指向SEH链的头部,所以每一个线程都有自己的SEH链,不同线程之间的SEH链也不同。每个线程创建之前都会安装顶层异常处理过程,而每一个顶层异常处理程序的过滤函数都是UnhandledExceptionFilter(),此函数会在内部调用全局变量kernel32!BasepCurrentTopLeveFilter保存的函数地址,此函数可以干预UnhandleExceptionFilter()的返回值。而我们可以调用SetUnhandledExceptionFilter()函数改变此全局变量的值为自定义函数的地址。所以顶层异常处理是基于进程的(顶层异常过滤函数返回值干预函数的值是全局变量保存的)。
- 如果程序是用MSC编译器编译的程序默认的异常处理函数一般是Kernel32!_except_handlerX(),Kernel32!_except_handlerX()会先调用UnhandleExceptionFilter()过滤函数,而如果我们在UnhandleExceptionFilter()过滤函数中已经把此异常处理了则就不会执行默认的异常处理,否则默认的异常处理就是结束应用程序。
顶层异常处理在反调试中的应用
-
为当程序产生一个异常时,首先会被系统内核捕捉然后系统内核会判断当前是否有调试器调试,有的话把异常交给调试器。如果没有调试器或者调试器处理不了此异常的话,异常会被分发给SEH链上的各个异常处理回调函数依次处理,这些回调函数的地址是通过一个链表存储。通过遍历这个链表从而调用各个异常处理回调函数。如果异常被某个回调函数正常处理了就继续从产生异常的代码处继续往下执行。如果一直遍历到链表的最后一个异常回调函数之前都没能够处理此异常的话,此异常就会交给最后一个默认的异常处理程序处理。
-
而最后一个默认异常处理函数在调用时会先调用UnhandledExceptionFilter( )过滤函数,此函数又会调用ZwQueryInformationProcess( )函数先判断是否有调试器存在,有的话会直接返回进行异常的二次分发(一般就是会结束进程)。
-
如果ZwQueryInformationProcess()函数没有检测到调试器的存在的话其将会执行默认异常处理。而一般默认异常处理是默认的终止程序的函数。(如果没有调试器的话异常是不会进行二次分发的,这点很重要)
-
但是windows提供一个函数来对UnhandledExceptionFilter( )过滤函数进行干预从而修改其返回值让其正常返回,而不执行默认的异常处理终止程序。此函数就是SetUnhandledExceptionFilter( ),此函数具有唯一的参数就是设置用来干预UnhandledExceptionFilter( )过滤函数的回调函数的地址,UnhandledExceptionFilter( )会在内部调用这个函数。(一般称这个函数为顶级异常处理函数,我认为这么称是不准确的。因为真正的顶级异常处理函数是默认的异常处理函数,此函数应该称为顶级过滤干预函数)
所以一般会利用此顶层异常处理函数的特性,主动产生异常,然后调用SetUnhandledExceptionFilter()设置顶层过滤函数的干扰函数来处理异常,如果用户调式程序的话,因为顶层异常处理会检测到有调试器,因此不会调用顶层过滤干扰函数来处理异常,导致异常无法处理从而终止运行。(需要我们改变ZwQueryInformationProcess( )函数的返回值来骗过UnhandledExceptionFilter( )函数让他以为无调试器)
传统的SEH
所谓传统的SEH结构化异常处理就是没有被编译器处理过,就是最纯正的SEH结构化异常处理。通过汇编语言来编写最原始的SEH结构化异常处理.
安装SEH,其中_Handler2是此异常处理的回调函数(调用约定为_cdecl)
push offset _Handler2
push fs:[0]
mov fs:[0],esp
卸载SEH
pop fs:[0] ;恢复SEH链
pop eax ;平衡堆栈
以下面汇编程序为例
start:
assume fs:nothing ;开始安装SEH异常处理程序
push offset _Handler3 ;设置异常处理回调函数的地址
push fs:[0] ;把下一个EXCETION_POINTERS结构
mov fs:[0],esp ;设置SEH链的头指针指向当前设置的EXCEPTION_POINTERS结构
push offset _Handler2
push fs:[0]
mov fs:[0],esp
push offset _Handler1
push fs:[0]
mov fs:[0],esp
pop fs:[0]
pop eax
pop fs:[0]
pop eax
pop fs:[0] ;恢复SEH链
pop eax ;平衡堆栈
invoke MessageBox,NULL,addr szDelete,NULL,MB_OK
invoke ExitProcess,NULL
end start
用OD查看程序,可以看到在没有执行程序前还没安装我们的SEH时,SEH链已经有数据。因为我们这个编译程序用的也是MSC编译器的ml.exe编译程序编译的,所以查看发现是程序也会安装的默认的异常处理回调函数是_except_handler4_command()。
然后我们执行程序,安装三个SEH后再查看SEH链,发现新增了3个结点。而且各个SEH链结点的异常回调函数地址为我们push的函数的地址
这就是传统的SEH链
编译器处理过的SEH
已MSC编译器为例,对于C而言编译器会在所有函数的入口点将SEH的异常处理回调函数都设置为_except_handlerX()。此函数的执行流程为:
- 编译器会在栈上为每一个_try块设置其索引,在函数一开始会设置_try的索引为-1。_except_handlerX()函数会获取当前_try的索引,如果_try索引为-1就表示异常没有发生在_try块中,表示此函数不能处理该异常,_except_handlerX()函数返回值为ExceptionContinueSearch表示不能处理此异常。然后异常就会沿着SEH链向上传递。
- 如果_try索引不为-1则说明异常发生在在_try块中,其再通过调用此_try对应的_except()里的过滤函数,然后根据过滤函数返回值决定是否调用_except()中的代码进行异常处理。
- 如果过滤函数的返回值能处理异常就去调用对应_except()内部的异常处理函数,如果过滤函数的返回值不能处理次异常就继续寻找上一层_try块(父_try块)的索引。直到最后寻找到最上层_try块的上面,即默认的_try索引为-1的地方,此时表示此函数已经不能处理该异常了,_except_handlerX()函数返回值为ExceptionContinueSearch表示不能处理此异常。然后异常就会沿着SEH链向上传递。
已下面c程序为例
#include <iostream>
#include <Windows.h>
using namespace std;
int main(int argc, char* argv[])
{
_try
{
cout<<"第三层try块"<<endl;
_try
{
cout<<"第二层try块"<<endl;
_try
{
cout<<"第一层try块"<<endl;
}
_except(EXCEPTION_CONTINUE_SEARCH)
{
}
}
_except(EXCEPTION_CONTINUE_SEARCH)
{
}
}
_except(EXCEPTION_CONTINUE_SEARCH)
{
}
return 0;
}
然后在OD中查看SEH链。发现SEH链上的各个异常处理回调函数都为_except_handler4(),此异常处理回调函数起到了一个代理的作用来完成_try和_except()处理。因为程序入口点为C运行库函数,C运行库函数在调用main函数,所以SEH链上有两个异常处理函数都是_except_handler4()。
ntdll.77C9B130是默认的异常处理回调函数,和0x00CE105A(_except_handler4())一样其会在内部调用_except_handler4_command(),只不过传递的参数不同。
有一点需要注意,当异常发生时或在SEH链中传递时,先调用异常处理回调函数_except_handler4(),然后在调用各个异常过滤函数,即_except()括号中的表达式判断能不能处理异常,如果返回值能处理就调用_except()内的异常处理代码。
参考资料:《看雪加密与解密》