概念

全称 Structured Exception Handling

是windows操作系统默认的异常处理机制;

使用

使用 _try 包裹可能出现异常的语句

_except()处理异常,当括号内为真的时候,执行处理语句;

例如:

1
2
3
4
5
6
7
8
9
10
11
12
int main()
{
_try
{
printf("123\n");
}
_except(1)
{
printf("执行!\n");
}
return 0;
}

原理

当程序触发异常后,程序会进行ip寄存器的跳转:

非调试状态下运行程序,触发后判断是否存在异常处理器(一个函数,在上述例子中,写上了try和except()编译器给程序添加了异常处理器,并会处理except中的内容),否则退出程序;

调试状态下,操作系统会优先将异常抛给调试进程(断点原理),之后调试器的选择有:

  • 修改触发异常的代码继续执行
  • 忽略异常交给SEH执行

则windows发生异常后的处理顺序为:调试器,SEH,结束程序;

结构

1
2
3
4
5
typedef struct _EXCEPTION_REGISTRATION_RECORD
{
struct _EXCEPTION_REGISTRATION_RECORD *Next; //指向下一个节点
PEXCEPTION_ROUTINE Handler; //异常处理器(回调处理函数)
}EXCEPTION_REGISTRATION_RECORD;

这是一个链表结构中的节点,所以SEH是以链式存在的;

第一个节点位置位于 fs:[0] 段寄存器处(同时是TEB结构,即TEB第一个字段就是异常处理链);

当异常发生时,从第一个节点开始处理,之后向后传递依次处理,直到处理成功,可以返回原本位置继续执行,否则退出程序;

最后一个节点next指针指向 0xFFFFFFFF;

当在程序中写入了_try和_except之后,操作系统会动态的生成一个节点结构,从头部插入;

总结

要提一嘴的是,windows异常抛出的种类特别多,而调试器的设置有很重要的因素,在逆向的时候,遇到一些异常(比如c0005,0地址执行),直接运行过去会导致断点在SEH已经处理完的时候,并不会断在异常发生的时候,这和调试器的异常捕获设置有关;

调试器一般就只会在CC断点异常处断下;

也可以找到fs:[0]的地方,将断点直接打在SEH链表头部,这样发生异常就能断下来;

对于32位程序来说,用高级语言写的_try_except生成的新节点插入会在汇编中以如下的形式体现:

1
2
3
4
push ExceptionHandler			;编译器生成的异常处理器
mov eax, dword ptr fs:[0] ;原先的SEH链表头部
push eax
mov dword ptr fs:[0], esp

此时在栈中,原先SEH头部地址在上,相当于新节点的next,编译器给的函数在下,相当于新节点的Handler,此时esp指向新节点的第一个字段next,也就是新节点头部,所以将esp又给予fs:[0],为原链表在头部添加了一个新节点;

但要注意这只在这个函数体(栈帧)里有效,函数结束时会做出相应的栈平衡,并释放栈;