Windows 异常机制

异常的分类

(1)软件异常:由操作系统 / 应用程序 引发

用户:RaiseException -> RltRaiseException -> NtRaiseException -> KiRaiseException 
内核:RtlRaiseException -> NtRaiseException -> KiRaiseException

软件异常归根结底都是基于 RaiseException 这个用户态 API 和 NtRaiseException 的内核服务建立起来的。

void RaiseException(
	DWORD dwExceptionCode ,			// 异常状态码
	DWORD dwExceptionFlags,			// 异常标志(可持续 / 不可持续)
	DWORD nNumberofArguments,		// lpArguments[] 数组长度
	const DWORD* lpArguments		// 传递给异常处理程序的筛选表达式
);
// 具体作用就是将异常信息传递给 EXCEPTION_RECORD 这个结构,然后再调用 RtlRaiseException 函数
/*
	typedef struct _EXCEPTION_RECORD {
	  DWORD                     ExceptionCode;			// 异常状态码
	  DWORD                     ExceptionFlags;			// 异常标志
	  struct _EXCEPTION_RECORD  *ExceptionRecord;		// 指向下一个异常的指针
	  PVOID                     ExceptionAddress;		// 保存异常发生的地址
	  DWORD                     NumberParameters;		// ExceptionInformation[] 数组参数个数
	  ULONG_PTR                 ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];		// 异常描述信息
} EXCEPTION_RECORD;
*/

(2)硬件异常:由 CPU 引发

① 错误异常
处理错误异常时,操作系统首先保存当前环境(比如寄存器的值),然后调用相应的异常处理函数,如果异常处理成功则恢复现场环境继续执行。

② 陷阱异常
和错误异常不同,陷阱异常发生时会保存要执行的下一条指令的环境(而不是正在执行的指令的环境),调试器的断点就是基于陷阱异常实现的。

③ 终止异常
·主要用来处理严重的硬件错误,和上面的异常不同,这种异常不会恢复执行而是直接退出。

异常和中断的关系

中断可以再任何时候发生,和 CPU 正在执行什么指令无关,可以被取消。而异常是由于 CPU 执行了某条指令引起的,不能被取消。

异常的分发处理

异常产生时,CPU是通过IDT进入内核来寻找处理函数:
(1)内核可以处理这个异常
异常处理程序执行完后会恢复现场并继续执行,这个过程我们感知不到。
(2)内核不能处理这个异常
① 如果这个异常来自内核,蓝屏。
② 如果这个异常来自应用程序,则异常处理权转交给应用程序的异常处理函数。如果程序处理了异常则程序继续执行,如果没有处理这个异常则程序崩溃。

上面的 ② 有两种可能:应用程序被调试 / 没有被调试 ——

<1> 如果程序被调试,则异常处理权限转交给调试器(应用程序将调试信息包装成调试事件并发送给调试器,调试器使用 WaitForDebugEvent 获得该调试事件),调试器经过一系列操作后调用 ContinueDebugEvent 继续执行程序。如果调试器处理了这个异常则程序继续执行,如果没有处理则将异常处理权限转交给应用程序(也就是 <2> )。

<2> 如果程序没有被调试,则将异常信息连同线程上下文环境送入程序的栈中,并调用 KiUserExceptionDisptcher (由 ntdll 导出,所有的异常分发都会走这个函数)但KiUserExceptionDisptcher 实际不执行什么功能,实际发挥作用的是 RtlExceptionDisptcher,之后如果 RtlExceptionDisptcher 返回成功则调用 ZwContinue 继续执行,否则调用 ZwRaiseException 结束进程(如果正在被调试的话就把异常再次抛给调试器)。

但是如果调试器还是没有处理这个异常呢?(是个狠人🙄)难道又要交给程序了吗(这样不就陷入一个死循环吗)?
但操作系统不傻,这时候操作系统会调用 ExceptionPort 通知 csrss.exe 弹出一个对话框:
在这里插入图片描述csrss.exe 是 Windows 子系统的灵魂,它监管着系统内运行着的所有Windows进程和线程,每个进程在创建后都要到它这里注册登记后方能运行,退出时也要到此报告注销。除了掌管着各个进程的“生死存亡”,CSRSS在桌面管理、终端登录、控制台管理、HardError报告、和DOS虚拟机等方面也起着重要作用。如果尝试强行杀死CSRSS进程,系统便会蓝屏。

RtlExceptionDisptcher 的具体工作如下:

首先遍历 VEH 链表,逐个执行异常处理器,一旦某个处理器处理成功则返回成功,线程继续运行。如果 VEH 链表遍历完毕异常仍然没有被处理则遍历 SEH 链表,再逐个执行 SEH 的异常处理器一旦某个处理器处理成功则返回成功,线程继续运行。
如果 VEH 和 SEH 都没有处理这个异常,则进程结束。


硬件异常会通过IDT去调用异常处理例程(一般为KiTrap系列函数)。
软件异常则是通过API的层层调用传递异常的信息。

但无论是硬件异常还是软件异常,最后都会走到 KiDispatchException

VEH(向量化异常处理)

在每个进程的 ntdll.dll 中,有一张 VEH 链表,可以向其中添加一个节点来注册自己的异常处理器(通过调用 AddVectordExceptionHandler):

PVOID AddVectoredExceptionHandler(
  ULONG                       First,		// 0则把处理器放在链表尾,否则把处理器放在链表头
  PVECTORED_EXCEPTION_HANDLER Handler		// 自己定义的异常处理函数指针
);
// 注意:Handler 是进程级的,也就是说进程内的所有异常都会调用 Handler 指向的函数。

SEH(结构化异常处理)

SEH是基于线程的一种处理机制,依赖于栈进行存储和查找,所以也被称作是基于栈帧的异常处理机制。(所以就是说它只能处理自己线程的异常,而不是像 VEH 那样可以影响整个进程)

SEH 其实姐可以理解为高级语言中的 __try{} __except(){}(或 try , catch),它的作用就是构造一个 SEH 节点,并将该节点放入 SEH 链表头部。这个节点的结构如下:
在这里插入图片描述SEH 链表的表头指针永远都在 fs:[0] 这个位置。

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

UEF 和 VCH

在这里插入图片描述

TopLevelEH(顶级异常处理)

本质上也是 SEH,在最顶层的SEH中,可以注册一个顶层异常处理器(和 SEH 不同的是它可以影响所有的线程)。

当 SEH 链表中的异常处理器都处理不了某个异常,在最顶层的SEH中就会检查是否注册了顶层异常处理,如果注册了顶级异常处理器,就会给 SEH “最后一次处理异常的机会” ,把异常抛给 TopLevelEH(但如果程序被调试时就会忽略 TopLevelEH)。


所有异常的必经之路 KiDispatchException

(图片来自网络)
在这里插入图片描述


VOID KiDispatchException(
		IN PEXCEPTION_RECORD 	ExceptionRecord,		// 描述异常
		IN PKEXCEPTION_FRAME 	ExceptionFrame,			// 描述异常发生时的处理器状态,对于x86结构总是NULL
		IN PKTRAP_FRAME 	TrapFrame,					// 陷阱帧,异常发生时处理器自动记录的当前处理器的状态
		IN KPROCESSOR_MODE 	PreviousMode,				// 触发异常代码的执行模式是用户模式还是内核模式
		IN BOOLEAN 	FirstChance 						// 否是第一轮分发这个异常(对于一个异常,Windows系统会最多分发两轮)
)

/*
	typedef struct _EXCEPTION_RECORD {
	  DWORD                    ExceptionCode;			// 异常发生的原因
	  DWORD                    ExceptionFlags;			// 是否为连续异常
	  struct _EXCEPTION_RECORD *ExceptionRecord;		// 当发生嵌套异常时提供附加信息
	  PVOID                    ExceptionAddress;		// 异常发生的地址
	  DWORD                    NumberParameters;		// 与异常关联的参数数目(最后一个参数的数组长度)
	  ULONG_PTR                ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];		// 描述异常的附加参数数组
	} EXCEPTION_RECORD;
*/ 

/*
	typedef struct _KTRAP_FRAME
	{
	     ULONG DbgEbp;
	     ULONG DbgEip;
	     ULONG DbgArgMark;
	     ULONG DbgArgPointer;
	     WORD TempSegCs;
	     UCHAR Logging;
	     UCHAR Reserved;
	     ULONG TempEsp;
	     ULONG Dr0;
	     ULONG Dr1;
	     ULONG Dr2;
	     ULONG Dr3;
	     ULONG Dr6;
	     ULONG Dr7;
	     ULONG SegGs;
	     ULONG SegEs;
	     ULONG SegDs;
	     ULONG Edx;
	     ULONG Ecx;
	     ULONG Eax;
	     ULONG PreviousPreviousMode;
	     PEXCEPTION_REGISTRATION_RECORD ExceptionList;
	     ULONG SegFs;
	     ULONG Edi;
	     ULONG Esi;
	     ULONG Ebx;
	     ULONG Ebp;
	     ULONG ErrCode;
	     ULONG Eip;
	     ULONG SegCs;
	     ULONG EFlags;
	     ULONG HardwareEsp;
	     ULONG HardwareSegSs;
	     ULONG V86Es;
	     ULONG V86Ds;
	     ULONG V86Fs;
	     ULONG V86Gs;
	} KTRAP_FRAME, *PKTRAP_FRAME;
*/

分发过程:

1)将 TrapFrame 参数备份到 Context 结构中

2)判断是内核模式(0)还是应用模式(1)

3)判断是否是第一次调用

4)判断是否有内核调试器(有的话交给调试器)

5)调用 RtlDispatchException 处理异常

保存 FS:0(KPCR -> ExceptionList),它指向一个结构:

typedef struct _EXCEPTION_REGISTRATION_RECORD{
	struct _EXCEPTION_REGISTRATION_RECORD *Next;		// 指向下一个_EXCEPTION_REGISTRATION_RECORD
	PEXCEPTION_ROUTINE Handler;							// 异常处理函数
} EXCEPTION_REGISTRATION_RECORD;

RtlDispatchException 的作用就是遍历这个链表,查找对应的异常处理函数。

6)RtlDispatchException 成功则正常执行,失败则再次判断有没有内核调试器,有则通知内核调试器,没有则蓝屏


内核态的异常处理

内核模式则查 IDT 表找到对应的异常处理函数

用户态的异常处理

调用 DbgkForwardException(将异常信息包装后发送给 r3 ):

  • 检擦调试端口(DebugPort)是否为空
  • 调用 DbgkSendApiMessage 将异常发送给 r3 调试器
  • 若 r3 无调试器或调试器处理不了此异常,则调用 KiUserExceptionDispatcher
    对于 KiUserExceptionDispatcher 而言,主要功能就是调用 RtIDispatchException 遍历 VEH 和 SEH,成功则调用 ZwContinue 进入 r0 ,失败则调用 ZwRaiseException 进行第二轮(最后一次机会)异常分发。

如果第二轮异常分发仍然失败,KidispatchException 就会终止当前线程。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值