文章目录
内核层异常的处理流程
之前已经了解过异常处理机制的执行流程:异常记录 异常分发 异常的处理。这次我们学习一下内核异常的分发与处理。
用户层异常和内核层异常
异常可以发生在用户空间,也可以发生在内核空间。无论是CPU异常还是模拟异常,是用户层异常还是内核层异常,都要通过KiDispatchException
函数进行分发。
我们首先来了解内核异常是如何分发的。
KiDispatchException函数详解
用IDA打开ntkrnlpa.exe,找到KiDispatchException
函数
核心的功能从这里开始,这个函数做的第一件事就是将Trap_frame备份到context 为返回三环做准备
接着判断先前模式,如果是0,则说明是内核层产生的异常,如果是1,则说明是用户层产生的异常。
这个函数用于处理用户层和内核层所有的异常,这个地方用于区分是三环的异常还是零环的异常。
接着判断是否是第一次调用
接着判断是否有内核调试器,如果有内核调试器,会先调用内核调试器。
如果这个函数的返回值为1,说明内核调试器已经处理,就将CONTEXT再转成Trap_Frame直接返回。
如果调试器没有处理,也就是返回0,直接跳转
如果没有内核调试器,或者内核调试器没有处理,就会调用RtlDispatchException
函数,这个函数专门负责调用异常处理函数来处理异常
接下来会判断RtlDispatchException
这个函数的返回值,如果返回失败,会再次判断是否有内核调试器。如果有内核调试器就调用这个内核调试器,如果没有的话则会进行跳转
系统直接蓝屏
KiDispatchException函数执行流程总结
- 将Trap_Frame备份到context为返回三环做准备
- 判断先前模式 0是内核调用 1是用户层调用
- 判断是否是第一次调用
- 判断是否有内核调试器
- 如果没有内核调试器则不处理
- 调用RtlDispatchException处理异常
- 如果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函数
流程总结:
- _KeContextFromKframes 将Trap_frame备份到context 为返回3环做准备
- 判断先前模式 0是内核调用 1是用户层调用
- 是否是第一次机会
- 是否有内核调试器
- 发送给3环调试器
- 如果3环调试器没有处理 这个异常 修正EIP为KiUserExceptionDispatcher
- KiDispatchException函数执行结束:CPU异常与模拟异常返回地点不同
- 无论通过那种方式,但线程再次回到3环时,将执行KiUserExceptionDispatcher函数