前言
上一篇学习了内核异常的分发,内核异常发生在内核层,处理起来比较简单,因为异常处理函数也在0环,不用切换堆栈;但是如果异常发生在3环,就意味着必须要切换堆栈,回到3环执行处理函数。
切换堆栈的处理方式与用户APC的执行过程几乎是一样的,唯一的区别就是执行用户APC时返回3环后执行的函数是KiUserApcDispatcher,而异常处理时返回3环后执行的函数是KiUserExceptionDispatcher。所以,理解用户APC的执行过程是理解3环异常处理的关键。
用户异常分发执行流程
无论是内核异常还是用户异常,都要通过KiDispatchException进行分发,所以从该函数开始分析。
备份TrapFrame
首先还是KiDispatchException的主体部分,上一篇在分析内核异常分发时已经分析过这里了,还是有两个点需要看一下:
- 红色方框:KeContextFromKframes函数将TrapFrame的值备份到Context结构中,以便在返回3环处理用户异常时,可以任意修改TrapFrame。此处ebx存着TrapFrame结构的首地址,在之后的代码分析中已将ebx替换成_KTRAP_FRAME使用。
- 橙色方框:在判断完先前模式后,若为用户模式,则从这里进行跳转。
内核调试器
若为用户异常,则会跳转至此处,这部分和内核异常的处理类似,主要是内核调试器的处理逻辑。
- 红色方框:这里主要是做几个判断,首先判断是否是第一次分发,若不是第一次分发,则跳走,本篇主要围绕第一次分发过程来讲。接下来判断是否存在内核调试器(例如Windbg),若不存在内核调试器,直接跳到下方代码继续执行。若存在调试器,则接着判断该调试器是否与当前线程关联的调试器是同一个。若不是同一个,就跳转了。
- 紫色方框:如果是同一个调试器,就会执行到这里。此时会push相应的参数,并调用内核调试器。判断内核调试器的值,如果值不为0,说明异常被内核调试器处理掉了。跳走,异常处理完毕,甚至没有返回3环的过程。如果值不为0,说明异常未被处理,则接着往下执行。
用户调试器
这里开始,是内核异常所没有的,内核异常是不会调用用户调试器的。
这里有一个函数非常关键,DbgkForwardException,先来看函数原型。
1 | BOOLEAN NTAPI DbgkForwardException( |
它接收3个参数,含义如下:
- ExceptionRecord:异常记录的结构,之前有讲。
- DebugPort:这个参数用来判断往哪个端口发送消息,若往调试端口发送,则值为TRUE;若往异常端口发送,则值为FALSE。
- SecondChance:判断是否为第二次分发,若是则为TRUE,反之为FALSE。
这个DbgkForwardException函数,用来将异常分发给用户调试器(这一步并没有返回3环),执行过程和内核调试器类似,它会先判断DebugPort字段是否为空,如果不为空,则调用DbgkSendApiMessage将异常发给调试子系统,后者又将异常发给用户调试器。如果用户调试器把异常处理掉了,那么DbgkForwardException会返回TRUE,异常分发过程也就结束了。否则,接着往下执行。
返回3环
若没有内核调试器或者内核调试器没处理,没有用户调试器或者用户调试器也没处理,就会走到这一部,接下来有一长串的代码,但是关键的只有一步。
返回3环处理,需要用到TrapFrame中保存的值,由于先前已将TrapFrame的值备份到Context结构中,所以这里可以任意修改TrapFrame的值,这部分的代码主要也就是修改TrapFrame,其中最为重要的,就是红色方框框住的修改TrapFrame.EIP的值。这个值决定了返回3环时开始执行的位置。它被设置成由内核变量KeUserExceptionDispatcher指向的一个三环函数KiUserExceptionDispatcher(属于Ntdll.dll)。返回三环后,便从KiUserExceptionDispatcher处开始执行。
至此,KiDispatcherException函数执行结束。需要注意的是,KiDispatchException并不完成返回3环的工作,它主要是将异常向不同的调试器进行分发,若调试器不存在或都不进行处理,则修改TrapFrame结构内的值,为返回3环坐准备。至于如何返回3环呢,观察下图:
由于不同类型的异常,调用KiDispatcherException的函数不同,所以会当KiDispatcherException执行完后,会返回当相应的函数继续执行。如果是CPU异常,则会返回到CommonDispatcherException函数中,并通过IRETD返回3环(CPU是通过中断门进的0环,因此用中断返回);如果是模拟异常,则会返回到NT!KiRaiseException函数中,并通过系统调用(KiServiceExit)返回3环。无论通过那种方式,线程再次回到3环时,将执行KiUserExceptionDispatcher。
3环异常处理
这部分就不详细分析,因跳转过多,仅作简要概括(这个坑以后考虑补上)。如图,回到3环后从KiUserExceptionDispatcher处开始执行,和处理内核异常类似,它会调用RtlDispatchException,该函数会在异常链表(VEH)上找异常处理器来处理异常,在链表的尾部保存着系统注册的一个默认的异常处理器。如果前面的异常处理器都没有处理异常,那么RtlDispatchException会找到默认异常处理器调用UnhandledExceptionFilter函数(例如用户界面弹出 “应用程序错误” 对话框来终止进程)。
如果RtlDispatchException将异常处理掉,这里会调用ZwContinue重新进入0环(类似用户APC)。
在没有调试的情况下,用户态异常不会经历第二轮分发。若当前进程正在被调试,那么UnhandledExceptionFilter会返回EXCEPTION_CONTINUE_SEARCH,导致RtlDispatchException返回FALSE。异常将进入第二轮分发。
第二轮分发
若RtlDispatchException返回FALSE,KiUserExceptionDispatcher会将FirstChance设置为FALSE,并进入第二轮异常分发。第二轮分发还是从KiDispatchException开始,第二次分发会调用两次DbgkForwardException函数,先后将异常发送给调试端口和异常端口(如果前者未处理异常),若两个端口都未能处理异常,则会调用KeBugCheckEx触发蓝屏异常。
总结
至此,用户异常的分发就分析完结了;第二次分发的过程介绍的比较简单,因为跳转比较麻烦,但是核心逻辑不多。(完整的执行逻辑可参考软件调试p247~p248)。以下为用户异常分发的流程图,以及异常分发的综合流程图。
参考资料
参考书籍:
- 《软件调试 卷2:Windows平台调试》p173,p246~p248 —— 张银奎
参考教程:
- 海哥中级预习班课程
参考链接:
- https://cataloc.gitee.io/blog/2020/08/10/%E7%94%A8%E6%88%B7APC%E6%89%A7%E8%A1%8C%E8%BF%87%E7%A8%8B/ (用户APC执行过程)
- https://blog.csdn.net/weixin_42052102/article/details/83513729 (CSDN-My classmates学习笔记)
- https://blog.csdn.net/qq_38474570/article/details/104346374 (CSDN-鬼手56学习笔记)
- https://doxygen.reactos.org/da/d59/dbgk_8h_source.html (ReactOS-dbgk.h)