@TOC
阅读加密解密的异常处理章节,总结。

异常处理的基本概念

当有程序发生异常的时候,CPU通过IDT(中断描述符)来寻找处理函数。
IDT是存在于物理内存的线性表,32位下每项长8字节,64位下每项长64字节
IDTR是用来描述IDT位置和长度的寄存器,一共48位,高32位是表的基质,低16位是表的长度。可以通过SIDT和LIDT指令来读取该寄存器,LIDT只能在ring0下运行
在这里插入图片描述
发生异常的时候,通过类型号执行相应的函数(kiTrapxx),同时还会将异常处理信息进行封装,
包括两部分:异常记录和陷阱帧

异常记录

在这里插入图片描述execeptioncode定义了异常产生的原因
在这里插入图片描述

陷阱帧

描述了异常时线程的状态,记录了每个寄存器的情况
此结构只在内核时使用,用户使用时会把陷阱帧复制一份命名为context来使用
在这里插入图片描述

处理过程

封装好后,会调用nt!KiDispatchException来继续处理异常
在这里插入图片描述在内核状态下和用户状态下有着不同的处理过程,在有调试器或者没有调试器也有着不同的处理过程

内核下产生异常&&没有调试器:
1调用nt!DispatchException,根据seh来处理异常
2处理不了的话,调用KeBugCheckEx函数产生蓝屏错误

内核下产生异常&&有调试器:
1把控制权转交给调试器,根据用户的对异常处理的设置来处理异常
2如果调试器选择不处理异常,就调用nt!DispatchException,根据seh来处理异常
3如果处理不了的话,再让调试器处理一次
4最后还处理不了,调用KeBugCheckEx函数产生蓝屏错误

用户态产生异常&&没有调试器
1在栈上放置exception_record和context结构,把控制权交给ntdll.dll的KiUserExceptionDispatcher函数,由它调用ntdll!RtDispatchException函数进行处理,这部分包括seh和veh。其中seh包括SetUnhandledExeceptionFilter函数设置的顶级异常处理。
2处理不了的话,顶级异常就会终止程序(显示应用程序错误的对话框)

用户态产生异常&&有调试器
1有调试器,调试器先获得处理异常的权限
2在栈上放置exception_record和context结构,把控制权交给ntdll.dll的KiUserExceptionDispatcher函数,由它调用ntdll!RtDispatchException函数进行处理,这部分包括seh和veh。其中seh不包括顶级异常处理
3处理不了的话,调试器获得第二次处理的机会
4还处理不了的话,把异常分发给进程的异常端口进行处理,该端口调用csrss.exe进行监听,监听到 错误后,显示应用程序错误的对话框,在终结程序之前还会再调用一遍异常处理过程来释放资源。

SEH

SEH结构化异常处理

相关数据结构

TIB(线程信息块)
TIB指向的就是SEH
TIB位于TEB结构体的头部,在32位下为fs:[0],在64位下为gs:[0]

_Exception_Registration_Recode
就是TIB指向的结构
在这里插入图片描述

第一个成员指向下一个结构体,第二个成员指向SEH的处理函数,从而形成一个链表,当发生异常的时候,就从fs:[0]获取异常处理的链表头,依次遍历整个链表,直到异常被处理
Seh链表只运行在头部进行增加和删除

_Exception_Recode && context
异常处理封装的消息

_Exception_pointers
包含_Exception_Recode和conext的结构体,方便用户获取
在这里插入图片描述

SEH的安装和卸载

seh的安装只能在链表的头部进行,需要在_Exception_Registration_Recode上填写一个新的结构,代码如下

1
2
3
4
assume fs:nothing
push offest SEHandler
push fs:[0]
mov fs:[0],esp

assume fs:nothing 是masm编译器的要求

push offest SEHandler 压入增加的处理函数
push fs:[0] 压入当前的SEH链表头
(两个push构成一个新的结构体)

mov fs:[0],esp 让fs:[0]指向栈顶
在这里插入图片描述
SEH的卸载

1
2
mov esp,dword ptr fs:[0]
pop dword ptr fs:[0]

改变指针的位置,使其指向第二个异常处理结构就行

SEH异常处理程序原理及设计

异常分发

异常处理的过程实际上就是系统将异常发送到各个异常处理单元进行处理的过程
用户的异常分发从ntdll!KiUserExceptionDispatcher函数开始
在这里插入图片描述

KiUserExceptionDispatcher调用RtDispatchException处理异常,如果成功的话,调用NtContinue恢复线程执行,如果失败的话引发第二次异常

RtDispatchException
函数逻辑较长
1调用VEH ExceptionHandler 进行处理
2处理不了的话,调用SEH继续进行处理
具体做法是逐一调用RtlpExecuteHandlerForException(负责执行SEHandler),根据不同的返回结果进行不同的处理
对于ExceptionContinueExceution 结束遍历并且返回
对于ExceptionContinueSearch 继续遍历下一个节点
对于ExceptionNestedException 从指定异常开始继续遍历
3调用VEH ContinueHandler进行处理

线程异常处理

线程异常处理就是指SEH异常处理
异常回调函数就是安装和卸载SEH的异常处理程序

在这里插入图片描述
这个回调函数通过exception_record判断异常能否处理已经后续步骤
返回exceptioncontinuexecution 代表问题已解决
返回exceptioncontinuesearch 代表不能处理该异常
返回exceptionnestedexception 处理异常时又发生了异常
返回exceptioncollideunwind 异常展开操作时发送了异常

异常处理的栈展开

栈展开的目的:1给程序清理未释放资源的机会 2避免发生未知的错误
为什么要进行栈展开:
就是在发生异常处理的过程中,如果把eip改为新的地址,而在新的地址处如果进行了一些压栈或者出栈的操作,那么就会改变fs:[0]指向的exception_registration结构,引发异常。
栈展开的步骤:
在这里插入图片描述

如何进行栈展开:
在这里插入图片描述

MSC编译器对线程异常处理的增强

c语言
MSC编辑器对_exception_registration_recode结构进行了扩充
例如vc6.0
在这里插入图片描述经过增加,MSC编辑器又引入了__try,__except,__finally
在这里插入图片描述except里的filterfunc()函数进行筛选异常,然后在对应的函数下进行处理

c++
在这里插入图片描述
try,catch,throw

顶级异常处理

顶级异常处理是系统设置的一个默认异常处理程序
操作系统在执行任意一个用户线程之前,都已经默认安装了一个SEH异常处理程序,是该线程的第一个SEH异常处理程序,也是SEH异常处理的最后一个

和顶级异常处理相关联的函数是UnhandledExceptionFilter
这个函数的大致步骤如下:
1对预定错误的预处理
如果异常嵌套异常,那么直接结束当前进程
检测异常代码是不是exception_access_violation和引起异常的操作是不是写操作,并且尝试恢复错误
检测调试器是否存在

2调用用户设置的回调函数
顶层异常回调函数>SetUnhandledExceptionFilter,给用户在顶级异常处理时一个机会

3进行终结处理

异常处理的安全性

SEH的结构是存储在栈上的,为了防止缓冲区溢出攻击微软增加了SafeSEH和SEHOP机制,阻止非法的SEHHandler执行。

SafeSEH
在编辑PE文件的时候可以打开这个开关,那么就会在PE头的DllCharacteristics中加入一个标志,并且提取所有异常处理的RVA,放入一个表里。这个表的指针是PE头部IMAGE_OPTIONAL_HEADER里第10项的一个结构体成员
在这里插入图片描述
SEHandlerTable指向rva表
SEHandlerCount指向表的大小
在PE文件载入时,系统把这两项保存在ntdll.dll里的一个表格中,异常发生时,就会拿出来检查检查
在这里插入图片描述
在这里插入图片描述
SEHOP机制
检测包括两点:
1检测SEH链的完整性,每一个节点都在栈上,并且可以正常访问
2检测最后一个节点的异常处理函数是不是位于ntdll中的ntdll!FinalExceptionHandler()

如何开启:修改注册表
在这里插入图片描述

向量化异常处理

可以通过api函数addVectoredExceptionHandler注册VEH回调函数
在这里插入图片描述
FirstHandler为0 则回调函数位于VEH链表的尾部
FirstHandler非0值 则回调函数位于VEH链表的前端
VEH回调函数的返回值需要保存下来,用于之后的卸载

VEH的回调函数所在模块被卸载后,系统不能自动将回调函数的地址从VEH链表上移除,需要自己来完成此工作
在这里插入图片描述
唯一的一个参数,正是前面保存的返回值

VEH和SEH的区别

VEH和SEH有哪些区别呢?
1 VEH在注册的时候可以选择在前端还是后端,而SEH只能在前端注册
2 VEH优先于SEH调用
3 SEH作用于线程 而VEH作用于进程
4 VEH不需要栈展开

x64平台的异常处理

总的来说,就是因为栈的不安全性,把Handler放入了一个只读表里
在这里插入图片描述