用户层和内核层异常的处理流程

内核层异常的处理流程

之前已经了解过异常处理机制的执行流程:异常记录 异常分发 异常的处理。这次我们学习一下内核异常的分发与处理。

用户层异常和内核层异常

异常可以发生在用户空间,也可以发生在内核空间。无论是CPU异常还是模拟异常,是用户层异常还是内核层异常,都要通过KiDispatchException函数进行分发。

我们首先来了解内核异常是如何分发的。

KiDispatchException函数详解

在这里插入图片描述

用IDA打开ntkrnlpa.exe,找到KiDispatchException函数

在这里插入图片描述

核心的功能从这里开始,这个函数做的第一件事就是将Trap_frame备份到context 为返回三环做准备

在这里插入图片描述

接着判断先前模式,如果是0,则说明是内核层产生的异常,如果是1,则说明是用户层产生的异常。

这个函数用于处理用户层和内核层所有的异常,这个地方用于区分是三环的异常还是零环的异常。

在这里插入图片描述

接着判断是否是第一次调用

在这里插入图片描述

接着判断是否有内核调试器,如果有内核调试器,会先调用内核调试器。

如果这个函数的返回值为1,说明内核调试器已经处理,就将CONTEXT再转成Trap_Frame直接返回。

如果调试器没有处理,也就是返回0,直接跳转

在这里插入图片描述

如果没有内核调试器,或者内核调试器没有处理,就会调用RtlDispatchException函数,这个函数专门负责调用异常处理函数来处理异常

在这里插入图片描述

接下来会判断RtlDispatchException这个函数的返回值,如果返回失败,会再次判断是否有内核调试器。如果有内核调试器就调用这个内核调试器,如果没有的话则会进行跳转

在这里插入图片描述

系统直接蓝屏

KiDispatchException函数执行流程总结

  1. 将Trap_Frame备份到context为返回三环做准备
  2. 判断先前模式 0是内核调用 1是用户层调用
  3. 判断是否是第一次调用
  4. 判断是否有内核调试器
  5. 如果没有内核调试器则不处理
  6. 调用RtlDispatchException处理异常
  7. 如果RtlDispatchException返回FALSE,再次判断是否有内核调试器,没有直接蓝屏

RtlDispatchException函数的执行流程

在这里插入图片描述

RtlDispatchException在内部会调用RtlpGetRegistrationHead,继续跟进这个函数

在这里插入图片描述

RtlpGetRegistrationHead将FS:0保存到eax之后返回。我们知道FS:0在零环的时候指向的是KPCR,而KPCR的第一个成员就是ExceptionList

ExceptionList这个成员是一个指针,,它指向了一个结构体 _EXCEPTION_REGISTRATION_RECORD

typedef struct _EXCEPTION_REGISTRATION_RECORD
{
	struct _EXCEPTION_REGISTRATION_RECORD *Next;
	PEXCEPTION_ROUTINE Handler;
} EXCEPTION_REGISTRATION_RECORD;

这个结构体有两个成员,第一个成员指向下一个_EXCEPTION_REGISTRATION_RECORD,如果没有下一个_EXCEPTION_REGISTRATION_RECORD结构体,那么这个地方的值是-1。第二个成员是异常处理函数。

在这里插入图片描述

RtlDispatchException的作用就是遍历异常链表,调用异常处理函数,如果异常被正确处理了,该函数返回1。如果当前异常处理函数不能处理该异常,那么调用下一个异常处理函数,以此类推。如果到最后也没有函数能处理这个异常,返回0。

用户层异常的处理流程

异常如果发生在内核层,处理起来比较简单,因为异常处理函数也在0环,不用切换堆栈,但是如果异常发生在3环,就意味着必须要切换堆栈,回到三环执行异常处理函数。

切换堆栈的处理方式与用户APC的执行过程几乎是一样的,唯一的区别就是执行用户APC时返回3环后执行的函数是KiUserApcDispatcher,而异常处理时返回3环后执行的函数是KiUserExceptionDispatcher

用户异常的处理流程

下面来分析KiDispatchException对于用户层异常是如何处理的。

VOID KiDispatchException(ExceptionRecord, ExceptionFrame, TrapFrame, PreviousMode, FirstChance)

在这里插入图片描述

首先将Trap_Frame备份到Context结构体

.text:004256C3                 cmp     byte ptr [ebp+arg_C], 0 

再接着判断先前模式,如果是0 说明是用户层的异常,如果是1 就是用户层的异常

.text:0042572E                 cmp     [ebp+FirstChance_1], 1

接着会判断是否是第一次调用

.text:00425738                 cmp     _KiDebugRoutine, edi ; 判断是否存在内核调试器

再次判断是否存在内核调试器

.text:00425777                 call    _KiDebugRoutine ; 调用内核调试器

如果存在内核调试器则调用内核调试器,将异常信息发给内核调试器

.text:004257AD                 push    edi
.text:004257AE                 push    1
.text:004257B0                 push    esi
.text:004257B1                 call    _DbgkForwardException@12 ; DbgkForwardException(x,x,x)

如果没有内核调试器,或者内核调试器没有处理,就会调用DbgkForwardException函数将异常发送给3环调试器。3环调试器如果不存在或者没有处理的话,就会开始修改寄存器,准备返回3环

.text:004258C7                 mov     eax, _KeUserExceptionDispatcher;
.text:004258CC                 mov     [ebx+68h], eax  ; 

其中最关键的修改是这两行,这里eax的值是一个全局变量KeUserExceptionDispatcher;在操作系统初始化的时候,会给这个全局变量赋一个值,这个值就是ntdll.KiUserExceptionDispatcher函数

流程总结:

  1. _KeContextFromKframes 将Trap_frame备份到context 为返回3环做准备
  2. 判断先前模式 0是内核调用 1是用户层调用
  3. 是否是第一次机会
  4. 是否有内核调试器
  5. 发送给3环调试器
  6. 如果3环调试器没有处理 这个异常 修正EIP为KiUserExceptionDispatcher
  7. KiDispatchException函数执行结束:CPU异常与模拟异常返回地点不同
  8. 无论通过那种方式,但线程再次回到3环时,将执行KiUserExceptionDispatcher函数
发布了109 篇原创文章 · 获赞 115 · 访问量 9万+

猜你喜欢

转载自blog.csdn.net/qq_38474570/article/details/104346374