Windows XP 异常处理(二)异常分发

ʕ •̀ o •́ ʔ

1 内核异常分发

1.1 KiDispatchException内核异常

对于内核模式异常,会给系统两次处理异常的机会。

如果这是第一次机会处理该异常,即 FirstChance 参数为 TRUE,那么,交给内核调试器处理该异常,若内核调试器处理了该异常,则返回当前函数,发生异常的指令流继续执行;若不存在内核调试器或内核调试器没处理该异常,则调用 RtlDispatchException 函数,试图将该异常分发到一个基于调用帧的异常处理器(call frame based handler)。只要能找到一个异常处理器能处理此异常,就返回。

若第一次机会该异常未被处理,则再给内核调试器一次机会,若这次仍未被处理,则进入错误检查(bugcheck)。因此,内核模式的异常必须被处理,一旦未被处理,则系统崩溃。

KiDispatchException函数内核处理部分流程总结:
① 将Trap_Frame备份到函数局部变量ContextFrame为返回三环做准备;
② 判断先前模式,0是内核调用、1是用户层调用;
③ 对于内核异常,判断是否是第一次用;
④ 是第一次,判断是否有内核调试器,如果有则调用内核调试器;
⑤ 如果没有内核调试器或内核调试器返回FALSE(没有处理异常),则调用RtlDispatchException处理异常;
⑥ 如果RtlDispatchException返回TRUE,则将ContextFrame恢复到Trap_Frame并返回。如果RtlDispatchException返回FALSE,则接下来的流程跟第二次处理一样:再次判断是否有内核调试器,没有或者内核调试器返回FALSE则直接蓝屏。

第二次异常只给内核调试器处理,不会交给函数RtlDispatchException

可以参考:用户异常与模拟异常的派发内核异常的分发内核学习-异常处理

函数前半段逆向分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
CONTEXT ContextFrame;
ContextFrame.ContextFlags = 0x10017;

if( PreviousMode == 1 || KdDebuggerEnabled != 0)
{
ContextFrame.ContextFlags = 0x1001F;
if(KeI386XMMIPresent != 0)
{
ContextFrame.ContextFlags = 0x1003F;
}
}

//UserMode: Context.ContextFlags = 0x1001F
//KernelMode:
// - KdDebuggerEnabled == 1, Context.ContextFlags = 0x1001F
// - KdDebuggerEnabled == 0, Context.ContextFlags = 0x10017

KeContextFromKframes(TrapFrame,0,&ContextFrame);
if(ExceptionRecord->ExceptionCode == 0x80000003)
ContextFrame.Eip--;
if(ExceptionRecord->ExceptionCode == 0x10000004)
ExceptionRecord->ExceptionCode = 0xC0000005;

12.png

13.png

14.png

1.2 nt!RtlDispatchException处理

异常第一次处理时(FirstChance == TRUE ),如果不存在内核调试器(KiDebugRoutine == NULL)或内核调试器没有把异常成功处理,将会调用函数RtlDispatchException去执行内核中的异常处理函数。该函数的具体实现要等到讲VEH、SEH时再详细展开

1
2
3
4
BOOLEAN __stdcall RtlDispatchException (
IN PEXCEPTION_RECORD ExceptionRecord, //KiDispatchException的实参,该结构存在KiRaiseException中
IN PCONTEXT ContextRecord //KiDispatchException中的局部变量,从TrapFrame拷贝的
)

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

FS:[0]异常链表指针ExceptionList指向一个_EXCEPTION_REGISTRATION_RECORD结构,该结构体必须位于当前线程的堆栈中。这个结构体有两个成员,第一个成员指向下一个_EXCEPTION_REGISTRATION_RECORD,如果没有下一个_EXCEPTION_REGISTRATION_RECORD结构体,那么这个地方的值是-1。第二个成员是异常处理函数。

15.png

1
2
3
4
5
kd> dt _EXCEPTION_REGISTRATION_RECORD -v
nt!_EXCEPTION_REGISTRATION_RECORD
struct _EXCEPTION_REGISTRATION_RECORD, 2 elements, 0x8 bytes
+0x000 Next : Ptr32 _EXCEPTION_REGISTRATION_RECORD
+0x004 Handler : Ptr32 _EXCEPTION_DISPOSITION

2 用户异常分发

异常如果发生在内核层,处理起来比较简单,因为异常处理函数也在0环,不用切换堆栈,但是如果异常发生在3环,就意味着必须要切换堆栈,回到3环执行处理函数。
切换堆栈的处理方式与用户APC的执行过程几乎是一样的,惟一的区别就是执行用户APC时返回3环后执行的函数KiUserApcDispatcher,而异常处理时返回3环后执行的函数是KiUserExceptionDispatcher

2.1 KiDispatchException用户异常

KiDispatchException函数内核处理部分流程总结:
① 将Trap_Frame备份到ContextFrame为返回3环做准备。
② 判断先前模式,0是内核调用、1是用户层调。
③ 用户调用,判断是否是第一次机会。
④ 第一次机会,判断是否有内核调试器。
⑤ 如果有内核调试器,且DebugPort == 0,就调用KiDebugRoutine进行内核调试。
⑥ 如果没有内核调试器,或者内核调试器没有处理异常,则使用DbgkForwardException函数将异常发送给进程调试器(调试器在0环处理)。

⑦ 如果进程调试器没有处理这个异常 ,则给返回3环做准备工作:

  1. 将在NT!RtlRaiseException中的EXCEPTION_RECORD所有数据拷贝到3环KiUserExceptionDispatcher函数堆栈(拷贝之前验证3环地址ProbeForWrite,内核地址无需调用ProbeForWrite);
  2. 紧接着将在KiDispatchException中的ContextFrame数据拷贝到3环KiUserExceptionDispatcher函数堆栈(拷贝之前验证3环地址ProbeForWrite);
  3. 紧接着将3环的EXCEPTION_RECORD、ContextFrame地址放到KiUserExceptionDispatcher函数栈顶。
  4. 修正KTRAP_FRAME.HardwareEsp = esp3,并修正Eip3为KiUserExceptionDispatcher;

⑧ KiDispatchException函数执行结束,从当前函数进行返回,CPU异常与模拟异常返回地点不同:

  • CPU异常:CPU检测到异常 --> 查IDT执行处理函数 --> CommonDispatchException --> KiDispatchException。
    则 KiDispatchException 通过IRETD返回3环。
  • 模拟异常:CxxThrowException --> RaiseException --> RtlRaiseException --> NT!NtRaiseException --> NT!KiRaiseException --> KiDispatchException。
    则 KiDispatchException 通过系统调用返回3环 。

⑨ 无论通过那种方式,但线程再次回到3环时,将执行 KiUserExceptionDispatcher 函数。

第二次异常处理是从3环又返回0环才处理,这里先不讲。

2.2 KiUserExceptionDispatcher

该函数主要功能如下:

  1. 调用 RtlDispatchException 查找并执行异常处理函数。

  2. 如果 RtlDispatchException 返回TRUE,调用 ZwContinue 再次进入0环,但线程再次返回3环时,从 CONTETX.Eip 开始执行。

  3. 如果 RtlDispatchException 返回FALSE,调用 ZwRaiseException 进行第二轮异常分发

17.png

该函数的执行过程及伪代码可参考《软件调试第二版卷2 24.3.2P618》。

2.3 ntdll!RtlDispatchException

  1. RtlDispatchException函数会调用RtlCallVectoredExceptionHandlers函数处理 VEH
  2. 如果在 VEH 中没有处理异常,则会调用 RtlpGetRegistrationHead 获取 FS:[0] 后,循环遍历异常链表,条件满足后调用 RtlpExecuteHandlerForException 处理 SEH
  3. 根据RtlpExecuteHandlerForException返回值判断下一步动作(switch-case)。

18.png

由图,处理用户异常的RtlDispatchException要多调用一个函数RtlCallVectoredExceptionHandlers。这个函数的作用就是在VEH链表中遍历,以便寻找对应的异常处理函数。(从Windows XP开始)

该函数的执行过程及伪代码可参考《软件调试第二版卷2 24.3.1P615》。

随着 Windows 系统的发展,RtlDispatchException 函数中也加入了一些新的功能,比如 VEH 支持、DEP ( Data Execution Prevent)支持、Server 2003 SP0 和 Windows XP SP2 所引入的 SAFESEH 支持等。清单 24-3 给出了针对目前版本的 Windows 系统(Windows Vista)更新后的伪代码。

2.4 VEH与SEH

什么是VEH与SEH呢,首先来说说SEH,在之前内核异常的分发中分析过处理内核异常的RtlDispatchException函数中,它的内部调用了RtlpGetRegistrationHead来查找一个异常链表,结构大致如下。

15.png

这就是SEH链表的大致结构,它是一种存在堆栈中的局部链表。本篇将要学习的VEH结构与之类似,只不过VEH是全局链表内核的RtlDispatchException函数中只会查找SEH,用户的RtlDispatchException会先查找VEH,若异常未能得到处理,才会找SEH。

参考:VEH

返回值

  • VEH,RtlCallVectoredExceptionHandlers :-1、0。
  • SEH,RtlpExecuteHandlerForException:0、1、2。

3 VEH

除了结构化异常处理,从 Windows XP 开始,Windows 还支持一种名为向量化异常处理(Vectored Exception Handling,VEH)的异常处理机制。与 SEH 既可以用在用户模式又可以用在内核模式不同,VEH 只能用在用户态程序中

RtlCallVectoredExceptionHandlers 函数中,会循环地从 VEH 链表取每一个异常处理函数执行,根据执行的返回结果 0/-1 判断异常是否已经处理了,然后进行下一步动作。

3.1 VEH结构

一、RtlpCalloutEntryList 链表头

Ntdll.dll 中的全局变量 RtlpCalloutEntryList 指向 VEH 链表头,链表中每一个节点成员是 VEH_REGISTRATION 结构:

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

PVECTORED_EXCEPTION_HANDLER 是一个函数指针,用来指向3环异常处理的回调函数。

1
2
3
typedef LONG (NTAPI *PVECTORED_EXCEPTION_HANDLER)(
struct _EXCEPTION_POINTERS *ExceptionInfo
);

RtlpCalloutEntryList->next == &RtlpCalloutEntryList 时表示空链表。

二、异常处理回调函数格式

VEH的基本思想是通过注册回调函数来接收和处理异常。回调函数的格式为:

1
2
3
LONG CALLBACK VectoredHandler(
PEXCEPTION_POINTERS ExceptionInfo
);

EXCEPTION_POINTERS 结构如下:

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

需要注意:异常处理回调函数在插入到 RtlpCalloutEntryList 链表前需要使用 RtlEncodePointer 函数进行加密。在调用异常回调函数前需要调用 RtlDecodePointer 进行解密

3.2 VEH回调函数的返回值

当一个3环的异常处理回调函数被调用之后,返回值有两个:0 、-1。

常量 取值 含义
EXCEPTION_CONTINUE_EXECUTION -1 异常处理完毕,恢复往下执行。
EXCEPTION_CONTINUE_SEARCH 0 异常未被处理,继续搜索。

关于VEH的检测可参考:Detecting anomalous Vectored Exception Handlers on Windows

3.3 RtlCallVectoredExceptionHandler的回值

RtlCallVectoredExceptionHandlers 返回值:

  • TRUE:直接返回,返回值为1。
  • FALSE:执行SEH。

3.4 练习:插入VEH解决除零异常

插入/移除SEH

我们可以在 SEH 链表中插入 VEH_REGISTRATION 以便用来劫持异常处理,如除零异常这种“未处理异常”会让3环的异常处理机制得到机会处理。

函数 AddVectoredExceptionHandlerRemoveVectoredExceptionHandler 可以在链表中插入/移除一个 VEH。

1
2
3
4
5
6
7
8
WINBASEAPI PVOID WINAPI AddVectoredExceptionHandler(
IN ULONG FirstHandler, //1:插入到链表头,0:插入到链表尾
IN PVECTORED_EXCEPTION_HANDLER VectoredHandler
);

WINBASEAPI ULONG WINAPI RemoveVectoredExceptionHandler(
IN PVOID VectorerHandler
);

div为无符号除法,idiv为有符号除法。(Win7 x64,VC++)

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
55
56
57
58
59
60
61
#include "stdafx.h"
#include <windows.h>

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

// 定义异常处理函数
LONG NTAPI VectExcepHandler(PEXCEPTION_POINTERS pExcepInfo)
{
bSuccess = true;

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

//修改发生异常时ecx的值(被除数)
pExcepInfo->ContextRecord->Ecx = 20;

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


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

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

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

printf("val = %d\n",val);
if(val == 5)
{
printf("异常修复成功!\n");
MessageBox(NULL,"VEH异常处理函数执行了...","VEH异常",MB_OK);
}else printf("异常修复失败!\n");

getchar();
return 0;
}

19.png

4 SEH

KiUserExceptionDispatcher 会调用 RtlDispatchException 函数来查找并调用异常处理函数,查找的顺序:
1、先查全局链表:VEH
2、再查局部链表:SEH

4.1 SEH结构

SEH 结构是串在 FS:[0] 链表上的,在 0 环 SEH 的结构_EXCEPTION_REGISTRATION_RECORD,该结构以及函数指针定义如下:

1
2
3
4
5
6
7
8
9
10
11
typedef struct _EXCEPTION_REGISTRATION_RECORD {
struct _EXCEPTION_REGISTRATION_RECORD *Next;
PEXCEPTION_ROUTINE Handler;
} EXCEPTION_REGISTRATION_RECORD;

typedef EXCEPTION_DISPOSITION (*PEXCEPTION_ROUTINE) (
IN struct _EXCEPTION_RECORD *ExceptionRecord,
IN PVOID EstablisherFrame,
IN OUT struct _CONTEXT *ContextRecord,
IN OUT PDISPATCHER_CONTEXT DispatcherContext
);

3 环 SEH 结构定义如下(结构名可以随便取):

1
2
3
4
5
struct _Exception_Registration_Record
{
struct _Exception_Registration_Record* Next; //指向下一个节点,最后一个赋值为 -1
void* Handler; //指向异常处理函数
}EXCEPTION_REGISTR_RECORD;

3 环的 SEH 异常处理回调函数定义如下:

1
2
3
4
5
6
7
EXCEPTION_DISPOSITION __cdecl Eexception_Handler	//注意调用约定
(
struct _EXCEPTION_RECORD *ExceptionRecord, //异常记录结构体
void* EstablisherFrame, //SEH结构体地址
struct _CONTEXT *ContextRecord, //存储异常发生时的上下文环境
PDISPATCHER_CONTEXT DispatcherContext //异常分发函数传递来的额外信息,一般用不到
)
  1. SEH 结构需要初始化(注册)在可能发生异常函数堆栈内(包含于当前线程堆栈)。
  2. 3 环 SEH 的回调函数一定不能在当前线程堆栈内(软件调试P617:异常处理器不应该是栈中的代码,这是为了支持 DEP 功能,以防止意外执行攻击程序动态布置在栈上的代码)。在0环没有这一条要求。

SEH 回调函数虽然已经在代码中定义好了,但是当前线程还没有调用到,所以还不在线程堆栈里面。当前函数堆栈有些空间是不可执行的,但是回调函数必须可执行,所以为了支持 DEP,SEH 不应该当前线程的堆栈中,以防止意外执行攻击程序动态布置在栈上的代码。这一点在逆向 ntdll!RtlDispatchException 时可以看到。

关于 SEH 的存放位置:

  1. 栈帧(stack frame):将异常处理器注册在所在函数的栈帧中。使用这种方式注册的异常处理经常称为基于帧的异常处理(fame based exception handling )。32 位的 Windows 系统(x86)使用的就是此种方式。
  2. 表格(table):将异常处理器的基本信息以表格的形式存储在可执行文件(PE)的数据段中,这种方式简称为基于表的异常处理(table based exception handling )。64 位 Windows 系统(x64)中的64 位程序使用了这种方式。

SEH 异常处理的另一个特征就是线程相关性。也就是说,异常的分发和处理是在线程范围内进行的,异常处理器的注册也是相对线程而言的。理解这一点对于理解系统寻找和调用异常处理器的方法很重要。

15.png

4.2 RtlpExecuteHandlerForException函数返回值

RtlpExecuteHandlerForException返回值为:0、1、2。

常量 取值 含义
ExceptionContinueExecution 0 恢复执行触发异常的代码
ExceptionContinueSearch 1 继续寻找下一个异常处理器
ExceptionNestedException 2 在调用处理器的过程中又发生了异常,即嵌套异常

属于EXCEPTION_DISPOSITION枚举类型的常量:

1
2
3
4
5
6
7
8
9
/*
* Exception disposition return values.
*/
typedef enum _EXCEPTION_DISPOSITION {
ExceptionContinueExecution, //0
ExceptionContinueSearch, //1
ExceptionNestedException, //2
ExceptionCollidedUnwind //3
} EXCEPTION_DISPOSITION;
  1. 0,ExceptionContinueExecution
    • 如果此时 ExceptionRecord.ExceptionFlags 包含 EXCEPTION_NONCONTINUABLE(1),则说明在试图恢复执行不可继续的异常,因此 RtlDispatchException 会调用 RtlRaiseException 再次抛出异常,异常的代码为STATUS_NONCONTINUABLE_EXCEPTION
    • ExceptionRecord.ExceptionFlags 不包含 EXCEPTION_NONCONTINUABLE(1),则 RtlpExecuteHandlerForException 返回TRUE,表示异常已解决。对于用户态异常,这会导致 KiuserExceptionDispatcher 函数调用 ZwContinue 服务,让 CPU 返回异常发生处继续执行。
  2. 对于无效返回值RtlDispatchException 会抛出新的异常,异常代码为 STATUS_INVALID_DISPOSITION(0xC0000026)RtlRaiseException 函数如果成功,便不会再返回到 RtlDispatchException 函数中。

4.3 练习:使用SEH解决int 3异常

int 3 断点属于陷阱类异常,当陷阱类异常触发时,产生异常的指令代码已经执行了,EXCEPTION_RECORD.ExceptionAddress 为产生异常的下一条即将执行的代码。

  1. 例1:

    1
    2
    3
    4
    00444209      CC            int3
    00444209 83EC 68 sub esp,0x68
    0044420C 53 push ebx
    0044420D 56 push esi

    int 3 断下来时,EXCEPTION_RECORD.ExceptionAddress 记录的地址为0x00444209。此时的 EXCEPTION_RECORD.ExceptionCode == 0x80000003,在函数 nt!KiDispatchException 中会 Context.Eip -= 1,也就是说如果调试器/用户不解决这个调试断点的话,程序会不停的执行int 3然后奔溃。

  2. 例2:OllyDbg中如果在如下代码 00444209 上按 F2 下断点:

    1
    2
    3
    00444209      83EC 68       sub esp,0x68
    0044420C 53 push ebx
    0044420D 56 push esi

    则断点断下来时,程序中的代码应该和例1一样,然后调试器单步 F7/8 时将会执行下一条指令。

下面的示例就使用 SEH 来解决一个int 3异常:

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
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
#include "stdafx.h"
#include <windows.h>

//最原始的 SEH 链表结构(这个结构怎么写都行)
typedef struct _EXCEPTION_REGISTR_RECORD
{
struct _EXCEPTION_REGISTR_RECORD* Next; //指向下一个节点,最后一个赋值为 -1
void* Handler; //指向异常处理函数
}EXCEPTION_REGISTR_RECORD,*PEXCEPTION_REGISTR_RECORD;

EXCEPTION_DISPOSITION __cdecl MyEexception_Handler
(
struct _EXCEPTION_RECORD *ExceptionRecord, //异常结构体
PVOID EstablisherFrame, //SEH结构体地址
struct _CONTEXT *ContextRecord, //存储异常发生时的各种寄存器的值 栈位置等
PVOID DispatcherContext
)
{
if (ExceptionRecord->ExceptionCode == 0x80000003)
{
ContextRecord->Eip += 1;
return ExceptionContinueExecution; //-1
}
return ExceptionContinueSearch; //0
}

int main()
{
DWORD temp;
EXCEPTION_REGISTR_RECORD ExceptionRegiRecord; //必须在当前线程堆栈的堆栈中

//fs[0]-> ExceptionList
__asm
{
mov eax, fs:[0]
mov temp, eax

lea ecx, ExceptionRegiRecord
mov fs:[0], ecx
}
//为SEH成员赋值
ExceptionRegiRecord.Next = (EXCEPTION_REGISTR_RECORD*)temp;
ExceptionRegiRecord.Handler = (PVOID)&MyEexception_Handler;

//创建异常
int val = 0;
__asm
{
push eax
int 3

mov eax, 999
mov val, eax
}

//摘除刚插入的SEH
__asm
{
mov eax, temp
mov fs:[0], eax
pop eax
}

if(val == 999)
{
MessageBox(NULL, "SEH异常处理函数执行了...", "SEH异常",NULL);
}else
MessageBox(NULL, "SEH异常处理函数没有执行...", "SEH异常",NULL);

printf("int 3 断点修复成功...\n");

getchar();
return 0;
}

20.png

软件调试:11.3.3中讲了KiDispatchException用户部分,24.3.2讲KiUserExceptionDispatcher的执行流程,24.3.1讲ntdll!RtlDispatchException,11.5讲VEH,24.3,24.4讲SEH过程。

看到24.4。

5 使用汇编代码挂入SEH

挂 SEH 的两个原则:

  1. SEH 结构必须注册在当前线程堆栈中。
  2. SEH 的回调函数不能汇编于当前线程堆栈中。

通过汇编代码在当前线程堆栈中注册一个 EXCEPTION_REGISTRATION_RECORD 结构:

1
2
3
push offset MyEexception_Handler	//SEH回调函数(处理器)的地址
push fs:[0] //前一个SEH结构(结构化异常处理器)的地址。使fs:[0]成为当前SEH->Next
mov fs:[0], esp //登记新的结构

将刚才注册的EXCEPTION_REGISTRATION_RECORD 结构注销:

1
2
3
mov eax, [esp]		//从栈顶取得前一个异常登记结构的地址
mov fs:[0], eax //将前一个异常结构的地址赋给 FS:[0]
add esp, 8 //清理栈上的异常登记结构,平衡堆栈

如下代码使用汇编代码挂入SEH解决除零异常:

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 <windows.h>

//SEH 回调函数
EXCEPTION_DISPOSITION __cdecl MyEexception_Handler
(
struct _EXCEPTION_RECORD *ExceptionRecord, //异常结构体
PVOID EstablisherFrame, //SEH结构体地址
struct _CONTEXT *ContextRecord, //存储异常发生时的各种寄存器的值 栈位置等
PVOID DispatcherContext
)
{
if (ExceptionRecord->ExceptionCode == 0xC0000094)
{
ContextRecord->Eip += 2;
return ExceptionContinueExecution; //-1
}
return ExceptionContinueSearch; //0
}

int main(int argc, char* argv[])
{
int val = 0;

__asm
{
push offset MyEexception_Handler; //将自己定义的SEH回调函数挂入链首
push fs:[0];
mov fs:[0], esp;

xor edx, edx; //构造除零异常
xor ecx, ecx;
mov eax, 100;
idiv ecx; //edx <- (eax/ecx)的余数,EDX:EAX 除以 ECX

mov eax, 999;
mov val, eax;

mov eax, [esp]; //恢复原始ExceptionList
mov fs:[0], eax;
add esp, 8; //平衡堆栈
}

if(val == 999)
{
MessageBox(NULL, "SEH异常处理函数执行了...", "SEH异常",NULL);
}else
MessageBox(NULL, "SEH异常处理函数没有执行...", "SEH异常",NULL);

printf("除零异常修复成功...\n");

getchar();
return 0;
}

23.png

6 异常执行流程图

异常处理流程2.png