SEH(结构体异常处理)是一个老生常谈的问题,今天就让小弟来谈谈自己的感想。可能个人的风格偏向于喜欢研究它的设计理念,所以就直接忽略了逆向的过程(我已经逆了N遍...)。
接下来,我会从这个两个角度去说明。(本文主要以x86为主)
先放一个总结的图,以便后续叙述。
操作系统的SEH设计理念
1. 介绍异常链表
站在操作系统的角度,寻找异常处理的设计是以 fs:[0]指向链表头结点,链表是由 EXCEPTION_REGISTRATION结构体串联的一个链表。
a. EXCEPTION_REGISTRATION结构体(以函数为单位)
struct EXCEPTION_REGISTRATION
{
struct _EXCEPTION_REGISTRATION *prev;
void (*handler)(PEXCEPTION_RECORD, PEXCEPTION_REGISTRATION, PCONTEXT, PEXCEPTION_RECORD);
}
b. 链表的效果图
注:-1表示是链表的结尾了,其中的Handler是操作系统放置上去的。(这个函数不是SEH研究的范畴了暂时不讨论...
2. 寻找处理函数过程
a. 寻找处理异常的结点
当异常发生时,操作系统(严格上讲应该是CPU..)会顺着 FS:[0]指向的链表去依次遍历每一个结点,寻找可以处理这个异常的结点。
b. 展开操作
从fs:[0]指向的结点开始,依次调用handler中的扫尾操作,直到步骤1寻找到的这个结点处。
可能会有人疑问,为什么要进行展开操作呢?
其实很简单,如果我们直接调用找到的结点handler,那么就会破坏掉这个结点之前的handler堆栈。如下图:
此时fun_two发生异常,堆栈是这样的形式,但是fun_two无法处理这个异常,只有fun_one可以处理,假设可以直接调用fun_one的handle,那么这个handler就有一定的几率覆盖fun_two的栈注册异常的空间,如果再有一个异常,那么直接GG!所以展开的操作,可以说成是清理空间-扫尾操作,让handler有一个安全的执行环境!
但是这么注册异常就有一个效率问题,比如我们写了N多个函数,而且还是各种嵌套。那么光找到这个结点就很耗费很多时间,而且再加上展开操作..这谁顶得住呀!!!
此时编译器就想办法了,为了提供效率等等问题,编译器就对注册的异常结点进行了扩展。接下来才是重头戏!!!
编译器扩展的设计理念
编译器看了看操作系统的设计德行,思考着自己如何进行扩展呢?然后就扩展了注册的异常结点。(其实结构体的设计是一个东西的基础也是最重要的部分,因为结构体代表的是数据之间的关系。代码只是处理规定了这些数据的处理过程。就像操作系统,把它比如为数据库不为过吧.....还有就是在逆向的时候如果什么数据关系都不知道....那就是瞎逆。 这里扯扯蛋....
1. 扩展的结构体
typedef struct _EXCEPTION_REGISTRATION PEXCEPTION_REGISTRATION;//扩展的异常结点结构
struct _EXCEPTION_REGISTRATION{
DOWRD old_esp;
PEXCEPTION_POINTERS xpointers;
struct _EXCEPTION_REGISTRATION *prev;//指向上一个结点位置
void (*handler)(PEXCEPTION_RECORD, PEXCEPTION_REGISTRATION, PCONTEXT, PEXCEPTION_RECORD);
struct scopetable_entry *scopetable;
int trylevel;
int _ebp;
};
scopetable_entry 这个结构内部又形成一个异常链表
+0x000 previousTryLevel : Uint4B //上一个结点的位置
+0x004 lpfnFilter : Ptr32 int //过滤函数
+0x008 lpfnHandler : Ptr32 int //处理函数
结构体中的前两个成员是__except_handler3函数中动态生成的。
old_esp:代表是执行执行lpfnHandler时的esp环境。
xpointers:是为了满足编译器的设计的两个函数 Getxxx--获取 EXCEPTION_RECORD Getxxx --获取 CONTEXT
prev:指向前一个EXCEPTION_REGISTRATION
handler:处理函数
scopetable:编译器自己定义的异常链表(基于数组)
previousTryLevel:指向上一个结点
lpfnFilter:相当于原来操作系统的handler
lpfnHandler:真正处理的函数
trylevel:编译器用来寻找当前异常是由哪个scopetable_entry处理
ebp:用于获取异常发生时堆栈的ebp位置,进而获取堆栈中的参数等等。
此时 fs:[0]链表变成了如下图所示:
2. 异常处理过程
当程序出现异常的时候,根据第一个图,最终会来到Registertion_Exception.handler处,但是此时这个是一个统一的函数_except_handler3,这个函数就像是进入编译器领地的大门。
1. _except_handler3流程讲解
a. 这个函数首先会根据 trylevel指定的 scopetable_entry开始寻找哪个entry处理这个异常。(是不是有点似曾相识....)
再解释一下 trylevel(附一个逆向分析的图):
b. 找到可以异常的scopetable_entry,开始调用 __global_unwind2,将fs:[0]指向这个上图这个胖胖的结点!(这是操作系统设计的)
c. 调用编译器自己设计的__local_unwind2的进行展开,调用_finally中释放资源了、析构了等等!
d. 最终调用找到了lpfnHandler来对异常进行处理。
注:在研究一个东西的时候层次很重要,就像是处理一个事情的时候角度问题很重要。做产品更应该如此!!!
本文只是讲解了大致思想,构建了这个全局观后,再去看什么内嵌异常了,展开过程中又异常了等等,很容易理解。
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)
最后于 2021-6-27 08:03
被烟花易冷丶编辑
,原因: