前言
前一篇学习了用户异常的分发过程,当用户异常产生后,内核函数KiDispatchException并不是像处理内核异常那样在0环直接进行处理,而是修改3环EIP为KiUserExceptionDispatcher函数后就结束了。这样,当线程再次回到3环时,将会从KiUserExceptionDispatcher函数开始执行。
本篇,从就KiUserExceptionDispatcher入手分析它是如何调用异常处理函数处理异常的,并了解学习与之相关的VEH的概念。
KiUserDispatchException
再回过来看这个函数,有3个主要的步骤:
- RtlDispatchException:查找并执行异常处理函数。
- ZwContinue:若RtlDispatchException返回真,说明异常被处理了,调用该函数,再次进入0环。
- ZwRaiseException:若RtlDispatchException返回假,说明异常未被处理,调用该函数,进行第二轮异常分发。
本篇主要来关注RtlDispatchException是如何处理用户异常的。
RtlDispatchException
用户异常分发与内核异常分发都会调用RtlDispatchException函数,但两者的实现逻辑是不同的。观察下图:
由图,处理用户异常的RtlDispatchException要多调用一个函数RtlCallVectoredExceptionHandlers。这个函数的作用就是在VEH链表中遍历,以便寻找对应的异常处理函数。
VEH与SEH
什么是VEH与SEH呢,首先来说说SEH,在之前内核异常的分发中分析过处理内核异常的RtlDispatchException函数中,它的内部调用了RtlpGetRegistrationHead来查找一个异常链表,结构大致如下。
这就是SEH链表的大致结构,它是一种存在堆栈中的局部链表。本篇将要学习的VEH结构与之类似,只不过VEH是全局链表。内核的RtlDispatchException函数中只会查找SEH,用户的RtlDispatchException会先查找VEH,若异常未能得到处理,才会找SEH。
VEH基础
基本思想
VEH的基本思想是通过注册如下原型的回调函数来接收和处理异常。
1 | LONG CALLBACK VectoredHandler( |
ExceptionInfo:指向EXCEPTION_POINTERS结构的指针。其结构如下
Code1
2
3
4typedef struct _EXCEPTION_POINTERS {
PEXCEPTION_RECORD ExceptionRecord; //异常记录
PCONTEXT ContextRecord; //异常发生时的线程上下文环境
};
VectoredHandler的返回值为EXCEPTION_CONTINUE_EXECUTION(-1,异常处理完毕,恢复执行)或者EXCEPTION_CONTINUE_SEARCH(0,异常未被处理,继续搜索)
回调函数注册
以VectoredHandler为原型的回调函数创建好后,就可以将其注册,即添加到VEH全局链表中,当遇到对应的用户异常时,就会被查找并调用。注册回调函数的原型如下:
1 | PVOID AddVectoredExceptionHandler( |
- FirstHandler:指定该回调函数的被调用顺序,若为0表示希望最后被调用;若为1表示希望最先被调用。若注册了多个回调函数,且FirstHandler都为1,那么最后注册的会最先被调用。
- VectorHandler:需要注册的回调函数。
在AddVectoredExceptionHandler内部,会为每个VEH分配一个如下结构:
1 | typedef struct _VEH_REGISTRATION{ |
- next:指向下一个VEH。
- prev:指向前一个VEH。
- pfnVeh:指向当前VEH的回调函数。
当有多个VEH时,这些VEH的_VEH_REGISTRATION结构组成一个环状链表。Ntdll.dll中的全局变量RtlpCalloutEntryList指向该链表的链表头(在0环中,则是通过FS:[0]找到ExceptionList再找到SEH的链表头)
回调函数注销
有注册就有注销,微软提供了RemoveVectoredExceptionHandler函数用于回调函数的注销,原型如下:
1 | PVOID RemoveVectoredExceptionHandler( |
VEH实验
有了VEH的基础,这里来做一个小实验。编写如下代码:(环境:Windows XP,IDE:VC++6.0)
1 |
|
为了更好理解这个代码,把代码分为了3个部分,分别来看:
- Part1:这部分对应前两行,定义了一个指向AddVectorExceptionHandler函数原型的函数指针,原因是AddVectorExceptionHandler函数XP之前的系统没有,所以为了代码兼容,不选择直接调用,而是获取函数地址(若存在),然后用定义的函数指针指向它。再调用函数指针就可以实现相应功能了。
- Part2:这部分定义异常处理函数的功能,该异常处理函数用来处理除零异常,这里有两处修改:
- 第一处:这部分注释掉了,用来修改发生异常时的EIP。前面提到VectExceptionHandle的参数指向EXCEPTION_POINTERS结构,该结构的ContextRecord参数记录了异常发生时的线程上下文环境,因此我们可以通过ContextRecord获取到EIP的值并修改它,这样处理完异常再次返回三环时,EIP将跳过原先发生异常的位置开始执行,这样异常就解决了。
- 第二处:同样通过ContextRecord修改寄存器的值,这次修改的是Ecx的值,由于除数为0导致除零异常的发生,所以只要将Ecx修改为一个非0的值,那么回到异常发生点重新执行时,就不会发生异常。
- Part3:这部分为主函数。首先通过动态获取地址的方式,构造我们自己的AddVectorExceptionHandler函数,并调用它将定义的异常处理函数插入到VEH中。接着构造一个除零异常,使得异常发生时,能够调用我们VEH中的函数。
可以看到,修改Ecx时,Edx的值为2,说明异常成功的处理了。注意一点,Edx的值为Eax/Ecx的余数,所以设置Ecx为98时,因Eax值为100,所以Edx值为2。
修改Eip的结果如图所示,相当于异常返回发生异常处时,跳过了两个字节,这样就没有进行除零运算,Edx的值则为原先设定的0。
参考资料
参考书籍:
- 《软件调试 卷2:Windows平台调试》p259~p264 —— 张银奎
参考教程:
- 海哥逆向中级预习班
参考链接:
- https://blog.csdn.net/weixin_42052102/article/details/83540134 (CSDN-My classmates学习笔记)
- https://blog.csdn.net/qq_38474570/article/details/104346421 (CSDN-鬼手56学习笔记)