avatar

Catalog
VEH

前言

前一篇学习了用户异常的分发过程,当用户异常产生后,内核函数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的基本思想是通过注册如下原型的回调函数来接收和处理异常

c
1
2
3
LONG CALLBACK VectoredHandler(
PEXCEPTION_POINTERS ExceptionInfo
);
  • ExceptionInfo:指向EXCEPTION_POINTERS结构的指针。其结构如下

    Code
    1
    2
    3
    4
    typedef struct _EXCEPTION_POINTERS {
    PEXCEPTION_RECORD ExceptionRecord; //异常记录
    PCONTEXT ContextRecord; //异常发生时的线程上下文环境
    };

VectoredHandler的返回值为EXCEPTION_CONTINUE_EXECUTION(-1,异常处理完毕,恢复执行)或者EXCEPTION_CONTINUE_SEARCH(0,异常未被处理,继续搜索)

回调函数注册

以VectoredHandler为原型的回调函数创建好后,就可以将其注册,即添加到VEH全局链表中,当遇到对应的用户异常时,就会被查找并调用。注册回调函数的原型如下:

c
1
2
3
4
PVOID AddVectoredExceptionHandler(
ULONG FirstHandler,
PVECTORED_EXCEPTION_HANDLER VectorerHandler
);
  • FirstHandler:指定该回调函数的被调用顺序,若为0表示希望最后被调用;若为1表示希望最先被调用。若注册了多个回调函数,且FirstHandler都为1,那么最后注册的会最先被调用。
  • VectorHandler:需要注册的回调函数。

在AddVectoredExceptionHandler内部,会为每个VEH分配一个如下结构:

c
1
2
3
4
5
typedef struct _VEH_REGISTRATION{
_VEH_REGISTRATION* next;
_VEH_REGISTRATION* prev;
PVECTORED_EXCEPTION_HANDLER pfnVeh;
}VEH_REGISTRATION, *PVEH_REGISTRATION;
  • next:指向下一个VEH。
  • prev:指向前一个VEH。
  • pfnVeh:指向当前VEH的回调函数。

当有多个VEH时,这些VEH的_VEH_REGISTRATION结构组成一个环状链表。Ntdll.dll中的全局变量RtlpCalloutEntryList指向该链表的链表头(在0环中,则是通过FS:[0]找到ExceptionList再找到SEH的链表头)

回调函数注销

有注册就有注销,微软提供了RemoveVectoredExceptionHandler函数用于回调函数的注销,原型如下:

c
1
2
3
PVOID RemoveVectoredExceptionHandler(
PVECTORED_EXCEPTION_HANDLER VectorerHandler
);

VEH实验

有了VEH的基础,这里来做一个小实验。编写如下代码:(环境:Windows XP,IDE:VC++6.0)

c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
#include "stdafx.h"
#include

//Part1: 定义指向AddVectorExceptionHandler函数原型的函数指针
typedef PVOID(NTAPI *FnAddVectoredExceptionHandler)(ULONG, _EXCEPTION_POINTERS*);
FnAddVectoredExceptionHandler MyAddVectoredExceptionHandler;

//Part2: 定义异常处理函数
LONG NTAPI VectExcepHandler(PEXCEPTION_POINTERS pExcepInfo)
{
MessageBox(NULL,"VEH异常处理函数执行了...","VEH异常",MB_OK);

//除0异常
if (pExcepInfo->ExceptionRecord->ExceptionCode == 0xC0000094)
{
//修改发生异常时代码的Eip
//pExcepInfo->ContextRecord->Eip = pExcepInfo->ContextRecord->Eip + 2;

//修改发生异常时ecx的值
pExcepInfo->ContextRecord->Ecx = 98;

//此处返回表示异常已处理
return EXCEPTION_CONTINUE_EXECUTION;
}
//此处返回表示异常未处理
return EXCEPTION_CONTINUE_SEARCH;
}

//Part3:主函数
int main()
{
//动态获取AddVectoredExceptionHandler函数地址,并将异常处理函数挂入VEH链表
HMODULE hModule = GetModuleHandle("Kernel32.dll");
MyAddVectoredExceptionHandler = (FnAddVectoredExceptionHandler)::GetProcAddress(hModule,"AddVectoredExceptionHandler");

//参数1表示插入VEH链的头部, 0插入到VEH链的尾部
MyAddVectoredExceptionHandler(0, (_EXCEPTION_POINTERS *)&VectExcepHandler);

//构造除0异常
int val = 0;
_asm
{
xor edx, edx
xor ecx, ecx
mov eax, 100
idiv ecx //edx <- (eax/ecx)的余数
mov val, edx
}

printf("val = %d\n",val);

getchar();
return 0;
}

为了更好理解这个代码,把代码分为了3个部分,分别来看:

  • Part1:这部分对应前两行,定义了一个指向AddVectorExceptionHandler函数原型的函数指针,原因是AddVectorExceptionHandler函数XP之前的系统没有,所以为了代码兼容,不选择直接调用,而是获取函数地址(若存在),然后用定义的函数指针指向它。再调用函数指针就可以实现相应功能了。
  • Part2:这部分定义异常处理函数的功能,该异常处理函数用来处理除零异常,这里有两处修改:
    • 第一处:这部分注释掉了,用来修改发生异常时的EIP。前面提到VectExceptionHandle的参数指向EXCEPTION_POINTERS结构,该结构的ContextRecord参数记录了异常发生时的线程上下文环境,因此我们可以通过ContextRecord获取到EIP的值并修改它,这样处理完异常再次返回三环时,EIP将跳过原先发生异常的位置开始执行,这样异常就解决了
    • 第二处:同样通过ContextRecord修改寄存器的值,这次修改的是Ecx的值,由于除数为0导致除零异常的发生,所以只要将Ecx修改为一个非0的值,那么回到异常发生点重新执行时,就不会发生异常。
  • Part3:这部分为主函数。首先通过动态获取地址的方式,构造我们自己的AddVectorExceptionHandler函数,并调用它将定义的异常处理函数插入到VEH中。接着构造一个除零异常,使得异常发生时,能够调用我们VEH中的函数。

修改Ecx:

可以看到,修改Ecx时,Edx的值为2,说明异常成功的处理了。注意一点,Edx的值为Eax/Ecx的余数,所以设置Ecx为98时,因Eax值为100,所以Edx值为2。

修改Eip:

修改Eip的结果如图所示,相当于异常返回发生异常处时,跳过了两个字节,这样就没有进行除零运算,Edx的值则为原先设定的0。

参考资料

参考书籍:

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

参考教程:

  • 海哥逆向中级预习班

参考链接:

  1. https://blog.csdn.net/weixin_42052102/article/details/83540134 (CSDN-My classmates学习笔记)
  2. https://blog.csdn.net/qq_38474570/article/details/104346421 (CSDN-鬼手56学习笔记)
Author: cataLoc
Link: http://cataloc.gitee.io/blog/2020/08/22/VEH/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.
Donate
  • 微信
    微信
  • 支付寶
    支付寶