SEH异常
SEH异常
1.原始SEH异常介绍
1)何为SEH异常
所谓“SEH异常”,其本质存储在栈中的一条异常处理链表,在用户模拟异常中,ntdll!RtlDispatchException函数先尝试VEH异常处理,如果未处理成功则调用SEH异常:
其
其一个结点就是一个_EXCEPTION_REGISTRATION_RECORD数据结构:
struct _EXCEPTION_REGISTRATION_RECORD {
struct _EXCEPTION_REGISTRATION_RECORD* Next; //0x0
enum _EXCEPTION_DISPOSITION (*Handler)(struct _EXCEPTION_RECORD* arg1, VOID* arg2, struct _CONTEXT* arg3, VOID* arg4);//0x4
};
如下是Ntdll!RtlExceptionDispatch的分析,其中涉及很多标志位,很复杂,如果有可能之后会来详细解释。
后面的该函数处理的流程图摘自《软件调试》这本书,本来自己想画出来,但是水平太菜画的太烂。
2)如何挂上SEH异常
之前我们介绍过VEH异常,其用有关函数AddVectoredExceptionHandler可以来实现手动挂上VEH异常,但是SEH如何挂上呢?
我们之后会介绍编译器对于SEH的扩展,但是在之前,只能用手动的汇编思路挂上SEH。
ExceptionList在TEB+0x0位置,而我们又知道,在三环下其FS寄存器指向TEB,因此通过FS:[0]获取,然后手动构造处理结点即可。
如下,其所构造的handler回调函数,其格式必须是固定的,这样我们就可以触发自己的SEH异常。
// 123.cpp : Defines the entry point for the console application. // #include "stdafx.h" #include <windows.h> #include <stdio.h> DWORD scratch; //自定义异常回调函数 EXCEPTION_DISPOSITION __cdecl my_except_handler ( struct _EXCEPTION_RECORD *_ExceptionRecord, void * _EstablisherFrame, struct _CONTEXT *_ContextRecord, void * _DispatcherContext ) { printf( "Hello from my exception handler/n" ); //修复异常,使EAX寄存器指向一个有效的地址 _ContextRecord->Eax = (DWORD)&scratch; //异常已修复,重新执行引发异常的指令 return ExceptionContinueExecution; } int main() { DWORD handler = (DWORD)my_except_handler; __asm { //创建_EXCEPTION_REGISTRATION_RECORD结构 push handler //Handler成员 push fs:[0] //Next成员,指向下一个异常帧 mov fs:[0],esp //安装SEH } __asm { xor eax,eax //EAX = 0 mov dword ptr [eax],1234h //写EAX指向的内存从而故意引发一个异常! } //异常已修复,此时scratch的值为0x1234 printf( "After writing! scratch=0x%08x/n",scratch); __asm { //移除_EXCEPTION_REGISTRATION_RECORD结构 pop dword ptr fs:[0] add esp,4 } return 0; }
2. 编译器对SEH异常拓展
我们之前介绍过SEH异常,但是可以发现其用起来略微复杂,最起码需要用到汇编知识,并且也不太直观。
为解决这个问题,Windows的编译器提供了一种_try_except_finally语法,其可以有效的进行处理。
1)异常过滤表达式
理解SEH异常的一个重要就是SEH的过滤表达式,其存在如下三个值:
// Defined values for the exception filter expression
#define EXCEPTION_EXECUTE_HANDLER 1 // 执行handler函数
#define EXCEPTION_CONTINUE_SEARCH 0 // 未能识别异常,继续调用SEH链往下
#define EXCEPTION_CONTINUE_EXECUTION (-1) // 执行完毕,继续按照 CONTEXT.Eip中的值来运行
即在过滤表达式中,你有一次机会对异常进行处理,判断处理结果如何再做下一步操作,注意要区分一般的SEH异常。
一般的SEH异常返回的是 _EXCEPTION_DISPOSITION,其定义如下:
typedef enum _EXCEPTION_DISPOSITION
{
ExceptionContinueExecution,
ExceptionContinueSearch,
ExceptionNestedException,
ExceptionCollidedUnwind
} EXCEPTION_DISPOSITION;
初学者很容易把这两个搞混(我开始被搞混了很久),之所以不理解,是因为没有深刻理解SEH拓展的汇编代码,之后我们会来详细进行分析。
2)拓展SEH样例代码:
// 123.cpp : Defines the entry point for the console application. // #include "stdafx.h" #include <windows.h> #include <stdio.h> DWORD scratch; int b; int filter(PEXCEPTION_POINTERS p,int ecode){ printf("启动过滤表达函数\r\n"); b=1; return EXCEPTION_CONTINUE_SEARCH; } int main() { _try{ b = 0; int a = 1 / b; printf("异常处理完成"); } _except(filter(GetExceptionInformation(),GetExceptionCode())){ printf("handler处理程序"); } return 0; }
3. 拓展SEH在栈中的数据结构的变化
之前的SEH结构结构如下:
struct _EXCEPTION_REGISTRATION_RECORD {
struct _EXCEPTION_REGISTRATION_RECORD* Next; //0x0
enum _EXCEPTION_DISPOSITION (*Handler)(struct _EXCEPTION_RECORD* arg1, VOID* arg2, struct _CONTEXT* arg3, VOID* arg4);//0x4
};
而现在的结构如下
struct _EXCEPTION_REGISTRATION{
struct _EXCEPTION_REGISTRATION_RECORD* Next; //0x0
enum _EXCEPTION_DISPOSITION (*Handler)(struct _EXCEPTION_RECORD* arg1, VOID* arg2, struct _CONTEXT* arg3, VOID* arg4);
//0x4
struct scopetable_entry * scopetable
int trylevel;
ine _ebp;
};
我们根据这个结构来查看反汇编代码:
可以发现,其编译器先处理SEH异常结构,再来提升堆栈。
另外,值得注意的是:Release版本会进行大量优化,但当出现_try{}_except(){},其不会对其进行优化,因为要保证堆栈结构。
要明白,其是拓展结构,并没有影响原来的结构,原来的结构在这里依然可以使用的,故其SEH拓展后的结构如下所示:
4._EXCEPTION_REGISTRATION 结构中的实现细节
我们之前介绍过,无论一个函数中有多少个Try,其只要一个_EXCEPTION_REGISTRATION结构体就好。
但是,我们肯定很好奇,其是如何实现的。下面,我们就来分析一下其是如何来实现的。
1)ScopeTable表结构
其是一串结构体数组,理解它的含义是理解SEH拓展的关键,结构体如下:
previousTryLevel – 上一个try的索引
lpfnFilter – except过滤表达式位置
lpfnHandler – except_handler执行函数
2)我们现在分析一层复杂的ScopeTable结构:
如下图,很明显,第一个previousTryLevel表示的是其存在上一层的嵌套,现在我们有一个问题,try0先执行还是try1先执行?
当然是try1先执行,然后沿着previousTryLevel找到try0的except,明白了这个逻辑再来看这张图就很好理解。
lpfnFilter指向其过滤表达式,lpfnHanler指向_except_handler,异常处理代码。
3)trylevel的含义
我们看其反汇编代码,当做的第一件事就是往trylevel中填写一个数字,我们在ScopeTable中看到其存在一个编号。
因此,很容易推断出 trylevel记录当前try所在的编号。
通过trylevel这个编号,进入表通过 (Scopetable+0x0c*trylevel) 计算,就很容易找到各个元素。
5. 拓展SEH的进一步理解(EXCEPTION_REGISTRATION的形成与局部展开)
如果从上面一路看下来,你一定会理解比较深刻,这里有几个误区一定要明确:
① 对于拓展的SEH异常,不要将except_handler与handler划等号,拓展SEH异常统一写入一个handler函数(我们之后会对此来进行逆向分析)。
② 一个函数只会产生一个结点EXCEPTION_REGISTRATION,并不是多少个try会产生多少个结点,因为通过这个扩展以及ScopeTable表一个足够;
至于如何正确找到执行,这就涉及全局展开与局部展开,也是我们下面要分析的重点。
1)拓展SEH的形成 – __SEH_prolog函数分析
前面已经说明过拓展SEH的结构,但是其是如何形成的呢?
编译器一般是手动构造的,我们之前分析反汇编代码就是,但是在Windows函数中存在一个 __SEH_prolog函数,通过分析它我们就可以很清楚地了解构造了。
看下面反汇编代码解析,就能了解其详细的构成经过:
2)局部展开 _local_unwind
所谓局部展开,就是当遇见goto,break,return 等跳出try_finally_域时,调用其当前所嵌套的_finally域;
如下,即使在try中return,其也会在finally中执行,这就是finally的意义,因此其需要展开上面所有嵌套的
ScopeTable表的其存在finally与except两种元素,其图表如下:
现在我们分析局部展开函数_local_unwind2,其思路比较明确,其核心思路就是判断Filter == 0 就执行函数,执行完返回到上一层。
其中一个_NLG_Return2()函数,其作用就是方便return返回使用,这样嵌套的思路就很明确。
6. 拓展SEH的进一步理解(全局展开) 现在分析全局展开,因为全局展开涉及异常链表的查询,比较复杂,因此单独拿出来讲解。
1)一个奇怪的调用方式
如下,按正常逻辑,其应该按照finally、filter1、filter2、handler依次执行,但是实际情况却是filter先开始循环执行。
其实,所谓全局展开,就是当出现异常时,寻找到正确的except_hander的处理方式。
2)except_handler3函数分析
我们在SEH异常中统一填入except_hander3函数,下面我们先来分析一下这个函数,如下图,之后
3)RtlUnwind函数分析
__global_unwind2函数最终会调用一个RtlUnwind函数,该函数内容比较杂乱,其大体流程如下