avatar

Catalog
内核异常的分发

前言

在之前的文章中提过,异常处理机制的执行流程分为

  • 异常记录
  • 异常的分发
  • 异常的处理

本篇开始,开始学习异常分发的过程。

用户层异常与内核层异常

异常可以发生在用户空间,也可以发生在内核空间。正如APC一样。

无论是CPU异常还是模拟异常,是用户层异常还是内核异常,都要通过KiDispatchException函数进行分发。理解这个函数是学习异常的核心。(就像APC处理都需要调用KiDeliverApc一样)

本篇先学习较为简单的内核层异常,因为它进入KiDispatcherException异常分发函数后,主要处理过程都在内核进行,不需要再返回三环,因而逻辑较为简单。

KiDispatchException

这是处理异常最重要的函数,就像处理APC的KiDeliverApc函数一样

函数原型

先从函数原型开始,了解每个参数的含义

c
1
2
3
4
5
6
7
VOID KiDispatchException (
IN PEXCEPTION_RECORD ExceptionRecord,
IN PKEXCEPTION_FRAME ExceptionFrame,
IN PKTRAP_FRAME TrapFrame,
IN KPROCESSOR_MODE PreviousMode,
IN BOOLEAN FirstChance
)
  • ExceptionRecord:异常记录做的事情就是初始化这个结构体。
  • ExceptionFrame:对于x86系统,这个值是NULL。
  • TrapFrame:这个就非常熟悉了,3环进0环时,保存的现场就都在这里面。
  • PreviousMode:先前模式,0表示内核模式,1表示用户模式。
  • FirstChance:判断是否是第一轮分发这个异常,对于一个异常,Windows系统最多分发两轮。1表示第一次分发,0表示第二次分发。

执行流程(内核部分)

接下来,进入IDA,分析KiDispatchException处理内核异常时的执行过程:

  1. 进入函数主体(KiDispatchException)

    这里作了裁剪,以便于查看。接下来还是按照方框来分析:

    • 红色方框:首先从这里看起,它做了一件事,将_Trap_Frame备份到Context里,这部分和处理用户APC时很像,调用的也是同一函数。

    • 橙色方框:这里会根据KiDispatchException参数PreviousMode(先前模式),判断是内核异常还是用户层异常。本篇只关注内核异常,所以这里不跳转

    • 绿色方框:这里根据KiDispatchException参数FirstChance,判断是不是第一次分发,如果不是第一次就跳走。

    • 紫色方框:这里比较关键,先看第一个紫色方框,它会先查看内核变量KiDebugRoutine标识的内核调试器是否存在(例如Windbg)

      • 如果不存在,就跳转

      • 如果内核调试器存在,它就会调用内核调试器,并判断返回值。如果返回值为1,说明异常成功被处理掉,接下来会将Context的内容返还给_Trap_Frame,异常处理过程结束,退出KiDispatchException函数。如果返回值为0,说明异常未被处理掉,则跳转

      • 值得注意的是,两个跳转的位置是一样的,即不存在内核调试器或者内核调试器未处理掉异常时,会跳转到同一个地方

    • 粉色方框:若不存在内核调试器或者内核调试器未处理掉异常时,会跳转到这里。此时会传递两个参数Context和ExceptionRecord,并调用RtlDispatchException函数。这是负责调用异常处理函数的函数。

  2. RtlDispatchException部分

    RtlDispatchException在内部调用了RtlpGetRegistrationHead,这个函数返回了fs:[0]处保存的值。根据之前学习,在0环fs:[0]指向的是KPCR,KPCR的第一个成员NtTib,而NtTib的第一个字段是ExceptionList,ExceptionList这个字段是一个指针,它指向了一个结构体_EXCEPTION_REGISTRATION_RECORD:

    c
    1
    2
    3
    4
    5
    typedef struct _EXCEPTION_REGISTRATION_RECORD
    {
    struct _EXCEPTION_REGISTRATION_RECORD *Next;
    PEXCEPTION_ROUTINE Handler;
    } EXCEPTION_REGISTRATION_RECORD;

    这个结构体有两个成员,第一个成员指向下一个_EXCEPTION_REGISTRATION_RECORD,如果没有下一个结构体,则此处值为-1。第二个成员是异常处理函数。其内部结构如下:

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

  3. 第二次分发

    • 绿色方框:这里是RtlDispatchException执行结束的地方。
    • 主要逻辑:然后跟着跳转,向下看,这部分有4处跳转,其中2处橙色跳转的地方是一样的,2处红色跳转的地方是一样的。逻辑如下,首先,会判断RtlDispatchException函数的返回值,若该值为1,说明异常已经被处理,则触发橙色跳转;若值不为1,继续向下执行。这时会再次判断KiDebugRoutine标识的内核调试器是否存在,这便是二次分发。若仍然没有内核调试器,则触发红色跳转。若有,则和第一次分发时的处理一样,再次判断内核调试器是否对异常进行处理,若处理完成,触发橙色跳转,否则,触发红色跳转。
    • 触发橙色跳转:若触发了橙色跳转,说明异常已被处理。接下来会调用KeContextToKframes将Context备份的内容返还给_Trap_Frame,异常分发结束。
    • 触发红色跳转:若触发了红色跳转,说明异常未被处理,将会调用KeBugCheckEx函数,系统蓝屏。

总结

内核异常的总体流程可以参考下图,当然用户异常的流程也在图中,下一篇会讲到。

参考资料

参考书籍:

  • 《软件调试 卷2:Windows平台调试》p244~p246 —— 张银奎

参考教程:

  • 海哥中级预习班课程

参考链接:

  1. https://blog.csdn.net/weixin_42052102/article/details/83507711 (CSDN-My classmates学习笔记)
  2. https://blog.csdn.net/qq_38474570/article/details/104346374 (CSDN-鬼手56学习笔记)
Author: cataLoc
Link: http://cataloc.gitee.io/blog/2020/08/20/%E5%86%85%E6%A0%B8%E5%BC%82%E5%B8%B8%E7%9A%84%E5%88%86%E5%8F%91/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.
Donate
  • 微信
    微信
  • 支付寶
    支付寶