Windows XP 异常处理(二)异常分发
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 | CONTEXT ContextFrame; |
1.2 nt!RtlDispatchException处理
异常第一次处理时(FirstChance == TRUE
),如果不存在内核调试器(KiDebugRoutine == NULL
)或内核调试器没有把异常成功处理,将会调用函数RtlDispatchException
去执行内核中的异常处理函数。该函数的具体实现要等到讲VEH、SEH时再详细展开。
1 | BOOLEAN __stdcall RtlDispatchException ( |
RtlDispatchException
函数作用:遍历异常链表,调用异常处理函数,如果异常被正确处理了,该函数返回1。如果当前异常处理函数不能处理该异常,那么调用下一个,以此类推。如果到最后也没有人处理这个异常,返回0。
FS:[0]
异常链表指针ExceptionList
指向一个_EXCEPTION_REGISTRATION_RECORD
结构,该结构体必须位于当前线程的堆栈中。这个结构体有两个成员,第一个成员指向下一个_EXCEPTION_REGISTRATION_RECORD
,如果没有下一个_EXCEPTION_REGISTRATION_RECORD
结构体,那么这个地方的值是-1
。第二个成员是异常处理函数。
1 | kd> dt _EXCEPTION_REGISTRATION_RECORD -v |
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环做准备工作:
- 将在NT!RtlRaiseException中的EXCEPTION_RECORD所有数据拷贝到3环KiUserExceptionDispatcher函数堆栈(拷贝之前验证3环地址ProbeForWrite,内核地址无需调用ProbeForWrite);
- 紧接着将在KiDispatchException中的ContextFrame数据拷贝到3环KiUserExceptionDispatcher函数堆栈(拷贝之前验证3环地址ProbeForWrite);
- 紧接着将3环的EXCEPTION_RECORD、ContextFrame地址放到KiUserExceptionDispatcher函数栈顶。
- 修正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
该函数主要功能如下:
调用
RtlDispatchException
查找并执行异常处理函数。如果
RtlDispatchException
返回TRUE
,调用ZwContinue
再次进入0环,但线程再次返回3环时,从CONTETX.Eip
开始执行。如果
RtlDispatchException
返回FALSE
,调用ZwRaiseException
进行第二轮异常分发。
该函数的执行过程及伪代码可参考《软件调试第二版卷2 24.3.2P618》。
2.3 ntdll!RtlDispatchException
RtlDispatchException
函数会调用RtlCallVectoredExceptionHandlers
函数处理 VEH。- 如果在 VEH 中没有处理异常,则会调用
RtlpGetRegistrationHead
获取FS:[0]
后,循环遍历异常链表,条件满足后调用RtlpExecuteHandlerForException
处理 SEH。 - 根据
RtlpExecuteHandlerForException
返回值判断下一步动作(switch-case
)。
由图,处理用户异常的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
来查找一个异常链表,结构大致如下。
这就是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 | typedef struct _VEH_REGISTRATION{ |
PVECTORED_EXCEPTION_HANDLER
是一个函数指针,用来指向3环异常处理的回调函数。
1 | typedef LONG (NTAPI *PVECTORED_EXCEPTION_HANDLER)( |
当 RtlpCalloutEntryList->next == &RtlpCalloutEntryList
时表示空链表。
二、异常处理回调函数格式
VEH的基本思想是通过注册回调函数来接收和处理异常。回调函数的格式为:
1 | LONG CALLBACK VectoredHandler( |
EXCEPTION_POINTERS
结构如下:
1 | typedef struct _EXCEPTION_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环的异常处理机制得到机会处理。
函数 AddVectoredExceptionHandler
、RemoveVectoredExceptionHandler
可以在链表中插入/移除一个 VEH。
1 | WINBASEAPI PVOID WINAPI AddVectoredExceptionHandler( |
div
为无符号除法,idiv
为有符号除法。(Win7 x64,VC++)
1 |
|
4 SEH
KiUserExceptionDispatcher 会调用 RtlDispatchException 函数来查找并调用异常处理函数,查找的顺序:
1、先查全局链表:VEH
2、再查局部链表:SEH
4.1 SEH结构
SEH 结构是串在 FS:[0]
链表上的,在 0 环 SEH 的结构是 _EXCEPTION_REGISTRATION_RECORD
,该结构以及函数指针定义如下:
1 | typedef struct _EXCEPTION_REGISTRATION_RECORD { |
3 环 SEH 结构定义如下(结构名可以随便取):
1 | struct _Exception_Registration_Record |
3 环的 SEH 异常处理回调函数定义如下:
1 | EXCEPTION_DISPOSITION __cdecl Eexception_Handler //注意调用约定 |
- SEH 结构需要初始化(注册)在可能发生异常函数堆栈内(包含于当前线程堆栈)。
- 3 环 SEH 的回调函数一定不能在当前线程堆栈内(软件调试P617:异常处理器不应该是栈中的代码,这是为了支持 DEP 功能,以防止意外执行攻击程序动态布置在栈上的代码)。在0环没有这一条要求。
SEH 回调函数虽然已经在代码中定义好了,但是当前线程还没有调用到,所以还不在线程堆栈里面。当前函数堆栈有些空间是不可执行的,但是回调函数必须可执行,所以为了支持 DEP,SEH 不应该当前线程的堆栈中,以防止意外执行攻击程序动态布置在栈上的代码。这一点在逆向 ntdll!RtlDispatchException
时可以看到。
关于 SEH 的存放位置:
- 栈帧(stack frame):将异常处理器注册在所在函数的栈帧中。使用这种方式注册的异常处理经常称为基于帧的异常处理(fame based exception handling )。32 位的 Windows 系统(x86)使用的就是此种方式。
- 表格(table):将异常处理器的基本信息以表格的形式存储在可执行文件(PE)的数据段中,这种方式简称为基于表的异常处理(table based exception handling )。64 位 Windows 系统(x64)中的64 位程序使用了这种方式。
SEH 异常处理的另一个特征就是线程相关性。也就是说,异常的分发和处理是在线程范围内进行的,异常处理器的注册也是相对线程而言的。理解这一点对于理解系统寻找和调用异常处理器的方法很重要。
4.2 RtlpExecuteHandlerForException函数返回值
RtlpExecuteHandlerForException
返回值为:0、1、2。
常量 | 取值 | 含义 |
---|---|---|
ExceptionContinueExecution | 0 | 恢复执行触发异常的代码 |
ExceptionContinueSearch | 1 | 继续寻找下一个异常处理器 |
ExceptionNestedException | 2 | 在调用处理器的过程中又发生了异常,即嵌套异常 |
属于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 返回异常发生处继续执行。
- 如果此时
- 对于无效返回值:
RtlDispatchException
会抛出新的异常,异常代码为STATUS_INVALID_DISPOSITION(0xC0000026)
。RtlRaiseException
函数如果成功,便不会再返回到RtlDispatchException
函数中。
4.3 练习:使用SEH解决int 3异常
int 3
断点属于陷阱类异常,当陷阱类异常触发时,产生异常的指令代码已经执行了,EXCEPTION_RECORD.ExceptionAddress
为产生异常的下一条即将执行的代码。
例1:
1
2
3
400444209 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:OllyDbg中如果在如下代码
00444209
上按F2
下断点:1
2
300444209 83EC 68 sub esp,0x68
0044420C 53 push ebx
0044420D 56 push esi则断点断下来时,程序中的代码应该和例1一样,然后调试器单步
F7/8
时将会执行下一条指令。
下面的示例就使用 SEH 来解决一个int 3
异常:
1 |
|
软件调试: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 的两个原则:
- SEH 结构必须注册在当前线程堆栈中。
- SEH 的回调函数不能汇编于当前线程堆栈中。
通过汇编代码在当前线程堆栈中注册一个 EXCEPTION_REGISTRATION_RECORD
结构:
1 | push offset MyEexception_Handler //SEH回调函数(处理器)的地址 |
将刚才注册的EXCEPTION_REGISTRATION_RECORD
结构注销:
1 | mov eax, [esp] //从栈顶取得前一个异常登记结构的地址 |
如下代码使用汇编代码挂入SEH解决除零异常:
1 |
|