SEH异常之编译器原理探究
_try_except原理
调用_except_handle3
这个异常处理函数,这里并不是每个编译器的异常处理函数都是相同的,然后存入结构体,将esp的值赋给fs:[0]
,再就是提升堆栈的操作
每个使用 _try _except
的函数,不管其内部嵌套或反复使用多少_try _except
,都只注册一遍,即只将一个 _EXCEPTION_REGISTRATION_RECORD
挂入当前线程的异常链表中(对于递归函数,每一次调用都会创建一个 _EXCEPTION_REGISTRATION_RECORD
,并挂入线程的异常链表中)。
typedef struct _EXCEPTION_REGISTRATION_RECORD {
struct _EXCEPTION_REGISTRATION_RECORD *Next;
PEXCEPTION_ROUTINE Handler;
} EXCEPTION_REGISTRATION_RECORD;
可以看到只有一个异常处理函数
那么这里编译器是如何做到只用一个异常处理函数的呢?编译器把原来_EXCEPTION_REGISTRATION_RECORD
结构进行了拓展,添加了三个成员
struct _EXCEPTION_REGISTRATION{
struct _EXCEPTION_REGISTRATION *prev;
void (*handler)(PEXCEPTION_RECORD, PEXCEPTION_REGISTRATION, PCONTEXT, PEXCEPTION_RECORD);
struct scopetable_entry *scopetable;
int trylevel;
int _ebp;
};
新堆栈结构如下
scopetable
struct scopetable_entry
{
DWORD previousTryLevel //上一个try{}结构编号
PDWRD lpfnFilter //过滤函数的起始地址
PDWRD lpfnHandler //异常处理程序的地址
}
查看地址可以发现有三个结构体
存储着的正式异常函数的开始地址和结束地址
第一个值previousTryLevel
是上一个try
结构的编号,这里如果在最外层就是-1,如果在第二层就是0,如果在第三层就是1,以此类推
trylevel
该成员表示代码运行到了哪个try
结构里面,进入一个try
则加1,try
结构执行完成之后则减1
_except_handler3
1.CPU检测到异常 -> 查中断表执行处理函数 -> CommonDispatchException
-> KiDispatchException
-> KiUserExceptionDispatcher
-> RtlDispatchException
->VEH
-> SEH
2.执行_except_handler3
函数
<1> 根据trylevel
选择scopetable
数组
<2> 调用scopetable
数组中对应的lpfnFilter
函数
1.EXCEPTION_EXECUTE_HANDLER(1) 执行except代码
2.EXCEPTION_CONTINUE_SEARCH(0) 寻找下一个
3.EXCEPTION_CONTINUE_EXECUTION(-1) 重新执行
<3> 如果lpfnFilter
函数返回0 向上遍历 直到previousTryLevel=-1
假设有两个异常点
首先找到trylevel
为0
然后找到异常过滤表达式为1
然后遍历数组的lpfnFilter
如果返回值为1则调用异常处理函数,如果为0则该异常函数不处理,如果为-1则继续从原异常点向下执行
假设在B这个地方出异常,得到trylevel
为2
那么这里就回去遍历lpfnFilter
为2的地方
假设这里返回值为0,则继续查找,注意这个地方是向上查找,首先判断当前previousTryLevel
的值是否为-1,如果为-1就停止查找(-1代表已经是最外层)try
结构,然后再向上找,假设这里返回值仍然为0,判断previousTryLevel
的值为-1,就停止查找,没有找到响应的异常处理函数
_try_finally原理
无论try
结构体中是什么代码,都会执行finally
里面的代码
// SEH6.cpp : Defines the entry point for the console application.
//
#include "stdafx.h"
#include <windows.h>
VOID ExceptionTest()
{
__try
{
return;
printf("Other code");
}
__finally
{
printf("Must run this code");
}
}
int main(int argc, char* argv[])
{
ExceptionTest();
getchar();
return 0;
}
局部展开
当try
里面没有异常,而是return
、continue
、break
等语句时,就不会走_except_handle3
这个函数,而是调用_local_unwind2
进行展开
然后调用[ebx + esi*4 + 8]
跟进去就到了finally
语句块的地方
我们探究一下实现的原理,这里本来应该是lpfnFilter
参数,指向异常处理过滤的代码的地址,但是这里是0。只要这个地方的地址为0就是finally
语句块
__global_unwind2
函数最终会调用一个RtlUnwind
函数,该函数内容比较杂乱,其大体流程如下
全局展开
// SEH6.cpp : Defines the entry point for the console application.
//
#include "stdafx.h"
#include <windows.h>
VOID ExceptionTest()
{
__try
{
__try
{
__try
{
*(int*)0 = 1;
}
__finally
{
printf("Must run this code : A");
}
}
__finally
{
printf("Must run this code : B");
}
}
__except(1)
{
printf("Here is Exception_functions");
}
}
int main(int argc, char* argv[])
{
ExceptionTest();
getchar();
return 0;
}
全局展开就是一层一层的向上找异常处理函数,finally
模块还是照常执行
未处理异常
入口程序的最后一道防线
这里调用mainCRTStartup()
,然后调用入口程序
相当于这里才是一个进程开始执行的地方
这里有一个call调用,跟进去看看
发现有修改fs:[0]
的操作,这里就相当于编译器为我们注册了一个异常处理函数
这里到kernel32.dll
里面的BaseProcessStart
里面看一下,这里有一个注册SEH异常处理函数的操作
线程启动的最后一道防线
// SEH7.cpp : Defines the entry point for the console application.
//
#include "stdafx.h"
#include <windows.h>
DWORD WINAPI ThreadProc(LPVOID lpParam)
{
int i = 1;
return 0;
}
int main(int argc, char* argv[])
{
CreateThread(NULL, 0, ThreadProc, NULL, 0, NULL);
getchar();
return 0;
可以发现线程也是从kernel32.dll
开始的
然后跟进调用
可以发现还是注册了一个异常处理函数
还是去IDA里面看BaseThreadStart
函数,发现也注册了一个SEH
异常的函数
UnhandledExceptionFilter
相当于编译器为我们生成了一段伪代码
__try
{
}
__except(UnhandledExceptionFilter(GetExceptionInformation())
{
//终止线程
//终止进程
}
只有程序被调试时,才会存在未处理异常
UnhandledExceptionFilter的执行流程:
1) 通过NtQueryInformationProcess查询当前进程是否正在被调试,如果是,返回EXCEPTION_CONTINUE_SEARCH,此时会进入第二轮分发
2) 如果没有被调试:
查询是否通过SetUnhandledExceptionFilter注册处理函数 如果有就调用
如果没有通过SetUnhandledExceptionFilter注册处理函数 弹出窗口 让用户选择终止程序还是启动即时调试器
如果用户没有启用即时调试器,那么该函数返回EXCEPTION_EXECUTE_HANDLER
SetUnhandledExceptionFilter
如果没有通过SetUnhandledExceptionFilter
注册异常处理函数,则程序崩溃
测试代码如下,我自己构造一个异常处理函数callback
并用SetUnhandledExceptionFilter
注册,构造一个除0异常,当没有被调试的时候就会调用callback
处理异常,然后继续正常运行,如果被调试则不会修复异常,因为这是最后一道防线,就会直接退出,起到反调试的效果
// SEH7.cpp : Defines the entry point for the console application.
//
#include "stdafx.h"
#include <windows.h>
long _stdcall callback(_EXCEPTION_POINTERS* excp)
{
excp->ContextRecord->Ecx = 1;
return EXCEPTION_CONTINUE_EXECUTION;
}
int main(int argc, char* argv[])
{
SetUnhandledExceptionFilter(callback);
_asm
{
xor edx,edx
xor ecx,ecx
mov eax,0x10
idiv ecx
}
printf("Run again!");
getchar();
return 0;
}
直接启动可以正常运行
使用od打开则直接退出
KiUserExceptionDispatcher
只有当前程序处于调试的时候才可能产生未处理异常
1) 调用RtlDispatchException 查找并执行异常处理函数
2) 如果RtlDispatchException返回真,调用ZwContinue再次进入0环,但线程再次返回3环时,会从修正后的位置开始执行。
3) 如果RtlDispatchException返回假,调用ZwRaiseException进行第二轮异常分发
(参见KiUserExceptionDispatcher代码)
往期推荐