前言
windows的SEH结构化异常处理是基于线程的,传统的SEH结构化异常会基于堆栈形成一条包含异常回调函数地址的链(SEH链)。而fs:[0](TEB的第一个字段)指向这条链的链头,当有异常发生时并产送到SEH处时其会从fs:[0]开始遍历这条链。如果那个链结点的回调函数能正确处理异常则程序会返回到异常发生处,否则继续遍历一直到链尾。
顶层异常处理
一般程序开始创建主线程运行前默认设置一个异常处理程序,类似于下面c语言代码的形式。其中过滤函数为UnhandledExceptionFilter( )。
_try
{
//程序入口
//main()返回退出程序
}
_except(UnhandledExceptionFilter( ))
{
//默认异常处理函数
}
所谓的顶层异常处理意思是,当程序的异常通过SEH链一直传递到最后一个我们自己设置的SEH链结点都没有处理此异常时,会调用这个默认的异常处理函数。
下面对顶层异常处理进行详细分析。
- SEH是基于线程的,因为每一个线程都有自己的TEB,又因为TEB的第一个字段指向SEH链的头部,所以每一个线程都有自己的SEH链,不同线程之间的SEH链也不同。而顶层异常处理是在进程创建之初,try块包含了整个进程,所以顶层异常处理是基于进程的(全局的)。
- 如果程序是用C编译器编译的顶层默认的异常处理函数一般是Kernel32!_except_handlerX(),此函数位于SEH链尾。
顶层异常处理在反调试中的应用
为当程序产生一个异常时,首先会被系统内核捕捉然后系统内核会判断当前是否有调试器调试,有的话把异常交给调试器。如果没有调试器或者调试器处理不了此异常的话,异常会被分发给SEH链上的各个异常处理回调函数依次处理,这些回调函数的地址是通过一个链表存储。通过遍历这个链表从而调用各个异常处理回调函数。如果异常被某个回调函数正常处理了就继续从产生异常的代码处继续往下执行。如果一直遍历到链表的最后一个异常回调函数之前都没能够处理此异常的话,此异常就会交给最后一个默认的异常处理程序处理。
而最后一个默认异常处理函数在调用前会先调用UnhandledExceptionFilter( )过滤函数,
此函数又会调用ZwQueryInformationProcess( )函数先判断是否有调试器存在,有的话会直接返回进行异常的二次分发(一般就是会结束进程)。
如果ZwQueryInformationProcess()函数没有检测到调试器的存在的话其将会调用默认异常处理回调函数。而一般默认异常处理回调函数是默认的终止程序的函数。(如果没有调试器的话异常时不会进行二次分发的。)
但是windows提供一个函数来对UnhandledExceptionFilter( )过滤函数进行干预从而修改其返回值让其正常返回,而不执行默认的异常回调函数终止程序。
此函数就是SetUnhandledExceptionFilter( ),此函数具有唯一的参数就是设置用来干预UnhandledExceptionFilter( )过滤函数的回调函数的地址,
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链已经有数据。查看发现是程序安装的默认的异常处理_except_handler4_command()
然后我们执行程序,安装三个SEH后再查看SEH链,发现新增了3个结点。而且各个SEH链结点的异常回调函数地址为我们push的函数的地址
这就是传统的SEH链
编译器处理过的SEH
已MSC编译器为例,编译器会把所以SEH的异常处理回调函数都设置为_except_handlerX(),然后内部通过他来调用except()里的过滤函数,然后根据过滤函数返回值决定是否调用except中的回调函数。
如果过滤函数的返回值不能处理次异常,则继续调用上一层try块。知道最后调用最上层try块,即默认的顶层过滤函数UnhandledExceptionFilter( )根据其返回值决定是否调用except中的默认的异常回调函数。
已下面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)
{
cout<<"第一层try块回调!";
}
}
_except(EXCEPTION_CONTINUE_SEARCH)
{
cout<<"第二层try块回调!";
}
}
_except(EXCEPTION_CONTINUE_SEARCH)
{
cout<<"第三层try快回调!";
}
return 0;
}
然后在OD中运行到第一层try处查看SEH链。发现SEH链上的各个异常处理函数都为_except_handler4_command()。可以看出这与传统的SEH结构化异常处理的不同,编译器利用这让seh链上只存在一个异常处理回调函数,即_except_handler4_command(),此函数实际起到的是一个代理函数的作用。
参考资料:《看雪加密与解密》