目录
前言
一、异常处理大致流程图
二、实验一:分析 KiTrap03
三、实验二:分析CommonDispatchException
四、代码探究:分析 KiDispatchException 函数
五、代码探究:伪代码分析用户层KiUserExceptionDispatcher
前言
在Windows操作系统的复杂运行机制中,异常处理是保障系统稳定和程序正常执行的关键环节。当程序运行出现诸如内存访问错误、除零操作等异常情况时,系统如何有条不紊地进行处理,将直接影响到用户体验和系统的可靠性。深入探究Windows异常分发原理,不仅有助于开发者更好地理解程序运行时的异常行为,还能帮助他们编写出更健壮、更稳定的代码。接下来,我们将通过具体的实验分析,揭开Windows异常分发原理的神秘面纱,深入了解系统在面对异常时的处理流程和机制,开始实验前需要自己配置好双机调试环境。
一、异常处理大致流程图
当发生异常时,CPU 根据异常号调用 IDT(中断描述符表)中对应的异常处理程序。
操作系统将各种异常处理函数填充到 IDT 中。等待异常处理函数被调用。
异常处理整体流程图:
异常处理用户层流程图:
二、实验一:分析 KiTrap03
当 CPU 执行完 `int3` 指令后,就会触发一个异常。一旦这个异常产生,操作系统便会从它的中断向量表中调用 3 号中断处理程序。这个 3 号中断处理程序对应的函数是 `nt!KiTrap03()` 。在 `nt!KiTrap03()` 函数执行了一系列的处理操作之后,它会接着调用 `nt!KiDisPatchException()` 函数来对异常进行分发处理。
`nt!KiDisPatchException()` 函数在开始处理异常时,首先会检查当前是否存在调试器,也就是判断程序是不是正在被调试。要是检测到有调试器存在,那么这个异常就会被移交给调试器来处理。等调试器把异常处理完毕后,程序会恢复到相应的状态继续执行。
然而,如果调试器不处理这个异常,或者根本就不存在调试器的情况下,那么这个异常就会被传递到程序自身的异常处理机制中。要是程序自身最终也没能成功处理这个异常,那么就会进行异常的第二次分发操作。
如下尝试解读:
nt!KiTrap03:
; 将立即数0压入栈,可能用于某些初始化或标记用途
83e885c0 6a 00 push 0
; 将0写入栈中偏移为esp + 2的字单元,可能用于特定数据结构的初始化
83e885c2 66 c7 44 24 02 00 00 mov word ptr [esp+2],0
; 保存ebp寄存器的值到栈,通常用于保存当前函数调用的栈帧基址
83e885c9 55 push ebp
; 保存ebx寄存器的值到栈,以便后续恢复使用
83e885ca 53 push ebx
; 保存esi寄存器的值到栈,以便后续恢复使用
83e885cb 56 push esi
; 保存edi寄存器的值到栈,以便后续恢复使用
83e885cc 57 push edi
; 保存fs段寄存器的值到栈,用于后续恢复段寄存器状态
83e885cd 0f a0 push fs
; 将30h赋值给ebx,可能用于指定某些系统数据结构的偏移或索引
83e885cf bb 30 00 00 00 mov ebx,30h
; 将bx(即ebx的低16位)的值赋给fs段寄存器,使fs指向KPCR(Kernel Processor Control Region)
83e885d4 66 8e e3 mov fs,bx ; ------》指向KPCR
; 从fs:[0]处读取双字数据并存入ebx,获取与当前线程相关的重要数据结构地址
83e885d7 64 8b 1d 00 00 00 00 mov ebx,dword ptr fs:[0]
; 将ebx的值压入栈,可能用于后续函数调用传递参数或保存中间结果
83e885de 53 push ebx
; esp减去4,预留4字节栈空间,可能用于局部变量或其他栈操作
83e885df 83 ec 04 sub esp,4
; 保存eax寄存器的值到栈,以便后续恢复使用
83e885e2 50 push eax
; 保存ecx寄存器的值到栈,以便后续恢复使用
83e885e3 51 push ecx
; 保存edx寄存器的值到栈,以便后续恢复使用
83e885e4 52 push edx
; 保存ds段寄存器的值到栈,用于后续恢复段寄存器状态
83e885e5 0f a8 push ds
; 保存es段寄存器的值到栈,用于后续恢复段寄存器状态
83e885e6 06 push es
; 保存gs段寄存器的值到栈,用于后续恢复段寄存器状态
83e885e7 0f a8 push gs
; 将23h赋值给ax(即eax的低16位),可能用于设置段选择子等用途
83e885e9 66 b8 23 00 mov ax,23h
; esp减去30h,预留48字节栈空间,用于构建陷阱帧,暂时不填充Dr寄存器以及调试寄存器
83e885ed 83 ec 30 sub esp,30h ; -》暂时不填充Dr寄存器以及调试寄存器
; 以上一系列操作都是在构造一个陷阱帧,用于保存异常发生时的寄存器状态等信息
; 将ax的值赋给ds段寄存器,设置数据段
83e885f0 66 8e d8 mov ds,ax
; 将ax的值赋给es段寄存器,设置附加数据段
83e885f3 66 8e c0 mov es,ax
; 将esp的值赋给ebp,设置当前函数栈帧的基址
83e885f6 8b ec mov ebp,esp
; 测试栈中偏移为esp + 70h处的双字数据的第17位,判断是否为虚拟86模式(20000h为100000000000000000B)
83e885f8 f7 44 24 70 00 00 02 00 test dword ptr [esp+70h],20000h
; 如果是虚拟86模式,则跳转到nt!V86_kit3_a(83e88598)
83e88600 75 96 jne nt!V86_kit3_a(83e88598) ; ----》是的话就跳转
; 从fs:[124h]处读取双字数据并存入ecx,获取当前线程对象的地址
83e88602 64 8b 0d 24 01 00 00 mov ecx,dword ptr fs:[124h] ; -----》当前线程对象
; 清除方向标志位,使字符串操作按正向进行
83e88609 fc cld
; 将栈中偏移为ebp + 2Ch处的双字数据与0进行与操作,用途暂不明确
83e8860a 83 65 2c 00 and dword ptr [ebp+2Ch],0 ; -----》不知道干什么
; 测试当前线程对象地址偏移3处的字节数据与ODFh进行与操作的结果,用途暂不明确
83e8860e f6 41 03 df test byte ptr [ecx+3],0DFh ; -----》不知道在检测啥
; 如果测试结果不为零,则跳转到nt!Dr_kit3_a(83e88518)
83e88612 0f 85 00 ff ff ff jne nt!Dr_kit3_a(83e88518)
; 从栈中偏移为ebp + 60h处读取双字数据并存入ebx,用途暂不明确
83e88618 8b 5d 60 mov ebx,dword ptr [ebp+60h]
; 从栈中偏移为ebp + 68h处读取双字数据并存入edi,用途暂不明确
83e8861b 8b 7d 68 mov edi,dword ptr [ebp+68h]
; 将edx的值存入栈中偏移为ebp + 0Ch处的双字单元,用途暂不明确
83e8861e 89 55 0c mov dword ptr [ebp+0Ch],edx
; 将0BADB0D00h存入栈中偏移为ebp + 8处的双字单元,用途暂不明确
83e88621 c7 45 08 00 0d db ba mov dword ptr [ebp+8],0BADB0D00h
; 将ebx的值存入栈中偏移为ebp处的双字单元,赋值给DebugEBP(可能用于调试相关的栈帧指针)
83e88628 89 5d 00 mov dword ptr [ebp],ebx ; ----》赋值DebugEBP
; 将edi的值存入栈中偏移为ebp + 4处的双字单元,赋值给DebugEIP(可能用于调试相关的指令指针)
83e8862b 89 7d 04 mov dword ptr [ebp+4],edi ; ----》赋值DebugEIP
; 比较nt!PoHiberInProgress(83f8c3d9)处的字节数据与0,PoHiberInProgress用途暂不明确,可能与硬件触发的中断有关
83e8862e 80 3d d9 c3 f8 83 00 cmp byte ptr [nt!PoHiberInProgress(83f8c3d9)],0
; 如果比较结果不相等,则跳转到nt!KiTrap03+0x7e(83e8863e)
83e88635 75 07 jne nt!KiTrap03+0x7e(83e8863e)
; 对nt!KiHardwareTrigger(83f821fc)处的双字数据进行原子加1操作,可能用于硬件触发相关的计数或同步
83e88637 f0 ff 05 fc 21 f8 83 lock inc dword ptr [nt!KiHardwareTrigger(83f821fc)]
; 将0赋值给eax,可能用于初始化或表示某种状态
83e8863e b8 00 00 00 00 mov eax,0
; 测试栈中偏移为ebp + 72h处的字节数据的第2位,检测是否有问题,有问题就不分发异常
83e88643 f6 45 72 02 test byte ptr [ebp+72h],2 ; -----》检测是不是有问题,有问题,就不分发异常
; 如果测试结果不为零,则跳转到nt!KiTrap03+0xb8(83e88678)
83e88647 75 2f jne nt!KiTrap03+0xb8(83e88678)
; 测试栈中偏移为ebp + 6Ch处的字节数据的第1位,检测CS段,判断是内核层还是用户层的异常
83e88649 f6 45 6c 01 test byte ptr [ebp+6Ch],1 ; --》检测CS段来判断内核层还是用户层的异常
; 如果测试结果不为零,则跳转到nt!KiTrap03+0x97(83e88657)
83e8864d 75 08 jne nt!KiTrap03+0x97(83e88657)
; 测试栈中偏移为ebp + 71h处的字节数据的第2位,检测Eflages的标志
83e8864f f6 45 71 02 test byte ptr [ebp+71h],2 ; --》检测Eflages的标志
; 如果测试结果为零,则跳转到nt!KiTrap03+0x9e(83e8865e)
83e88653 74 0a je nt!KiTrap03+0x9e(83e8865e)
; 无条件跳转到nt!KiTrap03+0x9f(83e8865f)
83e88655 eb 07 jmp nt!KiTrap03+0x9f(83e8865f)
; 比较栈中偏移为ebp + 6Ch处的字数据与1Bh,检测CS段
83e88657 66 83 7d 6c 1b cmp word ptr [ebp+6Ch],1Bh ; --》检测CS段
; 如果比较结果不相等,则跳转到nt!KiTrap03+0xb8(83e88678)
83e8865c 75 1a jne nt!KiTrap03+0xb8(83e88678)
; 开启中断,允许外部中断进入
83e8865e fb sti
; 将ecx的值赋给esi,可能用于数据传递或临时存储
83e8865f 8b f1 mov esi,ecx
; 将edx的值赋给edi,可能用于数据传递或临时存储
83e88661 8b fa mov edi,edx
; 将eax的值赋给edx,可能用于数据传递或临时存储
83e88663 8b d0 mov edx,eax
; 从栈中偏移为ebp + 68h处读取双字数据并存入ebx,获取EIP(指令指针)
83e88665 8b 5d 68 mov ebx,dword ptr [ebp+68h] ; ---------》EIP
; 将ebx的值减1,调整EIP,可能用于异常处理中的指令回溯
83e88668 4b dec ebx ; ------ 》EIP减1
; 将3赋值给ecx,设置异常号
83e88669 b9 03 00 00 00 mov ecx,3 ; ----------》异常号
; 将80000003h赋值给eax,设置错误码
83e8866e b8 03 00 00 80 mov eax,80000003h ; ----------》错误码
; 调用nt!(83e87d8c)进行通用分发异常,传递异常号、错误码等相关信息
83e88673 e8 14 f7 ff ff call nt!(83e87d8c) ; ---》通用分发异常
; 从fs:[124h]处读取双字数据并存入ebx,再次获取与当前线程相关的数据结构地址
83e88678 64 8b 1d 24 01 00 00 mov ebx,dword ptr fs:[124h]
; 从ebx + 50h处读取双字数据并存入ebx,进一步获取线程相关的特定数据
83e8867f 8b 5b 50 mov ebx,dword ptr [ebx+50h]
; 比较ebx + 148h处的双字数据与0,用途暂不明确
83e88682 83 bb 48 01 00 00 00 cmp dword ptr [ebx+148h],0
; 如果比较结果为零,则跳转到nt!KiTrap03+0x9e(83e8865e)
83e88689 74 d3 je nt!KiTrap03+0x9e(83e8865e)
; 将3压入栈,可能作为参数传递给后续函数调用
83e8868b 6a 03 push 3
; 调用nt!Ki386VdmReflectException_A(83e8ad98),可能用于处理与虚拟86模式相关的异常反射
83e8868d e8 06 27 00 00 call nt!Ki386VdmReflectException_A(83e8ad98)
; 测试ax与0FFFFh进行与操作的结果,用途暂不明确
83e88692 66 a9 ff ff test ax,0FFFFh
; 如果测试结果为零,则跳转到nt!KiTrap03+0x9f(83e8865f)
83e88696 74 c7 je nt!KiTrap03+0x9f(83e8865f)
; 跳转到nt!KiExceptionExit(83e87bf8),退出异常处理流程
83e88698 e9 5b f5 ff ff jmp nt!KiExceptionExit(83e87bf8) ; ----》退出
; 对ecx进行lea操作,实际上不改变ecx的值,可能用于保持指令对齐或编译器优化相关
83e8869d 8d 49 00 lea ecx,[ecx]
结论
1. 对于 `int3` 异常,负责处理它的函数是 `KiTrap03`。
2. 当异常处理开始的时候,首先会构建一个 `TRAP_FRAME` 陷阱帧结构。所谓陷阱帧,其实就是一个结构体,它的作用是存储系统调用、中断或者异常发生那一刻的寄存器状态信息。这样一来,当程序后续需要返回到用户空间或者回到中断发生的位置时,就能够依据陷阱帧里保存的信息,恢复那些寄存器的值,从而让程序可以继续运行下去。
3. 值得留意的是,`KiTrap03` 函数在执行过程中,实际上调用了 `CommonDispatchException` 函数来进一步处理异常相关的操作。
4.KiTrap03(IDT 中 3 号处理函数):相当于异常的源头,源码可参考WRK-v1.2,位于 base\ntos\ke\i386\trap.asm。
函数功能:开辟栈空间保存产生异常时的线程上下文。通过寄存器和栈传递参数来调用 CommonDispatchException 函数。寄存器传递:异常地址、异常代码、异常附加参数。栈传递:线程的上下文。
三、实验二:分析CommonDispatchException
函数功能:
开辟一个栈空间,将异常记录信息保存到栈中。
获取异常发生的模式(用户层 / 内核层)。
调用 KiDispatchException 函数,参数如下:
参数 1:异常记录结构体的地址
参数 2:NULL
参数 3:异常记录帧
参数 4:异常发生的模式
参数 5:是否是第一次处理异常(通过 KiTrapXX 系列函数处理的异常都是第一次)
nt!CommonDispatchException:
; 将栈指针 esp 减去 50h(十进制为 80),预留 80 字节的栈空间,用于后续存储局部变量或其他数据
83e87d8c 83 ec 50 sub esp,50h
; 将 eax 寄存器中的值(错误码)存储到栈顶([esp])位置,用于后续处理或传递错误码信息
83e87d8f 89 04 24 mov [esp],eax ; ------》错误码
; 使用异或操作将 eax 寄存器自身进行异或运算,结果为 0,相当于将 eax 清零
83e87d92 33 c0 xor eax,eax
; 将 eax 寄存器的值(此时为 0)存储到栈中偏移量为 esp + 4 的位置
83e87d94 89 44 24 04 mov [esp+4],eax ; -—--》0
; 将 eax 寄存器的值(为 0)存储到栈中偏移量为 esp + 8 的位置
83e87d98 89 44 24 08 mov [esp+8],eax ; ------》0
; 将 ebx 寄存器中的值(产生异常的地址)存储到栈中偏移量为 esp + 0Ch(十进制为 12)的位置
83e87d9c 89 5c 24 0c mov [esp+0Ch],ebx ; --------》产生异常的地址
; 将 ecx 寄存器中的值(异常号)存储到栈中偏移量为 esp + 10h(十进制为 16)的位置
83e87da0 89 4c 24 10 mov [esp+10h],ecx ; --------》异常号
; 比较 ecx 寄存器的值与 0,用于判断当前异常号是否为 0
83e87da4 83 f9 00 cmp ecx,0 ; ---------》如果是 0 号异常就掠过这里
; 如果 ecx 等于 0,则跳转到 nt!CommonDispatchException + 0x29(地址为 83e87db5)处执行
83e87da7 74 0c je nt!CommonDispatchException+0x29(83e87db5)
; 计算 esp + 14h(十进制为 20)的地址,并将结果存储到 ebx 寄存器中,可能用于后续访问栈上的特定数据
83e87da9 8d 5c 24 14 lea ebx,[esp+14h]
; 将 edx 寄存器中的值存储到 ebx 所指向的内存地址(双字)处
83e87dad 89 13 mov dword ptr [ebx],edx
; 将 esi 寄存器中的值存储到 ebx + 4 所指向的内存地址(双字)处
83e87daf 89 73 04 mov dword ptr [ebx+4],esi
; 将 edi 寄存器中的值存储到 ebx + 8 所指向的内存地址(双字)处
83e87db2 89 7b 08 mov dword ptr [ebx+8],edi
; 将当前栈指针 esp 的值赋给 ecx 寄存器,使得 ecx 指向当前的异常帧地址
83e87db5 8b cc mov ecx,esp ; -------》异常帧地址给于 ecx
; 测试栈中偏移量为 ebp + 72h 处的字节数据的第 2 位(从右往左数,最低位为第 0 位),
; 根据测试结果决定后续给 eax 存储 -1 还是 cs(可能与异常处理的某些标志位判断有关)
83e87db7 f6 45 72 02 test byte ptr [ebp+72h],2 ; --》判断 Eflag 的第二位,给 eax 存储 -1 还是 cs
; 如果测试结果为 0(即该位为 0),则跳转到 nt!CommonDispatchException + 0x38(地址为 83e87dc4)处执行
83e87dbb 74 07 je nt!CommonDispatchException+0x38(83e87dc4)
; 将 0FFFFh 赋值给 eax 寄存器,可能用于设置某种标志或状态值
83e87dbd b8 ff ff 00 00 mov eax,0FFFFh
; 无条件跳转到 nt!CommonDispatchException + 0x3b(地址为 83e87dc7)处执行
83e87dc2 eb 03 jmp nt!CommonDispatchException+0x3b(83e87dc7)
; 将栈中偏移量为 ebp + 6Ch(十进制为 108)处的双字数据读取到 eax 寄存器中
83e87dc4 8b 45 6c mov eax,dword ptr [ebp+6Ch]
; 将 eax 寄存器的值与 1 进行按位与运算,根据运算结果判断当前异常是用户层还是内核层的异常
83e87dc7 83 e0 01 and eax,1 ; -------》eax 根据 CS 和 1 做与运算从而得到是用户层还是内核层
; 将立即数 1 压入栈中,用于标记这是第一次分发异常(可能在后续处理中用于判断分发次数)
83e87dca 6a 01 push 1 ; -------》是第一次分发
; 将 eax 寄存器的值(表示内核层异常还是用户层异常的标志)压入栈中
83e87dcc 50 push eax ; -------》内核层异常还是用户层异常
; 将 ebp 寄存器的值(Trap_Frme 陷阱帧地址)压入栈中,用于保存当前栈帧的基址
83e87dcd 55 push ebp ; -------》Trap_Frme 陷阱帧地址
; 将立即数 0 压入栈中,可能用于表示异常帧地址的某种初始值或特定标识
83e87dce 6a 00 push 0 ; -------》异常帧地址
; 将 ecx 寄存器的值(异常记录,此时指向异常帧地址)压入栈中
83e87dd0 51 push ecx ; -------》异常记录,在栈上
; 调用 nt!KiDispatchException(地址为 83efdee0)函数,这是一个重要的异常分发函数,
; 之前压入栈中的数据可能作为参数传递给该函数进行进一步的异常处理
83e87dd1 e8 0a 61 07 00 call nt!KiDispatchException(83efdee0) ; --------》重要
总结:
通过查看 CS 段寄存器的最后两位来判断所发生的异常是属于用户异常,还是内核异常。
构建了一个用于记录异常相关信息的结构。
在完成上述操作后,实际调用了 KiDispatchException 函数,由它来对异常情况进行处理。
四、代码探究:分析 KiDispatchException 函数
代码参考WRK-v1.2,位于 base\ntos\ke\i386\exceptn.c
VOID
KiDispatchException (
IN PEXCEPTION_RECORD ExceptionRecord,
IN PKEXCEPTION_FRAME ExceptionFrame,
IN PKTRAP_FRAME TrapFrame,
IN KPROCESSOR_MODE PreviousMode,
IN BOOLEAN FirstChance
)
/*++
Routine Description:
This function is called to dispatch an exception to the proper mode and
to cause the exception dispatcher to be called. If the previous mode is
kernel, then the exception dispatcher is called directly to process the
exception. Otherwise the exception record, exception frame, and trap
frame contents are copied to the user mode stack. The contents of the
exception frame and trap are then modified such that when control is
returned, execution will commense in user mode in a routine which will
call the exception dispatcher.
该函数用于分发异常, 根据异常所产生的模式找到异常的处理函数. 如果异常发生在
内核模式, 函数会直接调用一个异常处理函数直接处理异常. 如果异常发生在用户模
式, 则函数会将异常记录, 异常栈帧, 和陷阱栈帧拷贝到用户模式的线程的栈 (这些
信息在用户态可以被修改, 被修改后会重新设置到线程环境上), 随后函数会进入到用
户态, 到了用户态之后会又专门的函数去处理异常.
Arguments:
ExceptionRecord - Supplies a pointer to an exception record.
ExceptionFrame - Supplies a pointer to an exception frame. For NT386,
this should be NULL.
TrapFrame - Supplies a pointer to a trap frame.
PreviousMode - Supplies the previous processor mode.
FirstChance - Supplies a boolean value that specifies whether this is
the first (TRUE) or second (FALSE) chance for the exception.
Return Value:
None.
--*/
{
CONTEXT ContextFrame; // 线程环境块
EXCEPTION_RECORD ExceptionRecord1, ExceptionRecord2; // 异常记录块
LONG Length;
ULONG UserStack1; // 保存用户态中栈上的一个地址
ULONG UserStack2; // 保存用户态中栈上的一个地址
//
// Move machine state from trap and exception frames to a context frame,
// and increment the number of exceptions dispatched.
// 获取发生异常的线程的线程上下文,输出到ContextRecord结构体变量中.
KeGetCurrentPrcb()->KeExceptionDispatchCount += 1;
ContextFrame.ContextFlags = CONTEXT_FULL | CONTEXT_DEBUG_REGISTERS;
if ((PreviousMode == UserMode) || KdDebuggerEnabled) {
//
// For usermode exceptions always try to dispatch the floating
// point state. This allows exception handlers & debuggers to
// examine/edit the npx context if required. Plus it allows
// exception handlers to use fp instructions without destroying
// the npx state at the time of the exception.
//
// Note: If there's no 80387, ContextTo/FromKFrames will use the
// emulator's current state. If the emulator can not give the
// current state, then the context_floating_point bit will be
// turned off by ContextFromKFrames.
//
ContextFrame.ContextFlags |= CONTEXT_FLOATING_POINT;
if (KeI386XMMIPresent) {
ContextFrame.ContextFlags |= CONTEXT_EXTENDED_REGISTERS;
}
}
KeContextFromKframes(TrapFrame, ExceptionFrame, &ContextFrame);
//
// if it is BREAK_POINT exception, we subtract 1 from EIP and report
// the updated EIP to user. This is because Cruiser requires EIP
// points to the int 3 instruction (not the instruction following int 3).
// In this case, BreakPoint exception is fatal. Otherwise we will step
// on the int 3 over and over again, if user does not handle it
//
// if the BREAK_POINT occured in V86 mode, the debugger running in the
// VDM will expect CS:EIP to point after the exception (the way the
// processor left it. this is also true for protected mode dos
// app debuggers. We will need a way to detect this.
//
//
switch (ExceptionRecord->ExceptionCode) {
case STATUS_BREAKPOINT: // 断点异常就将eip减到int3指令所在的首地址
ContextFrame.Eip--;
break;
case KI_EXCEPTION_ACCESS_VIOLATION: // 内存访问异常, 设置附加参数
ExceptionRecord->ExceptionCode = STATUS_ACCESS_VIOLATION;
if (PreviousMode == UserMode) {
if (KiCheckForAtlThunk(ExceptionRecord,&ContextFrame) != FALSE) {
goto Handled1;
}
if ((SharedUserData->ProcessorFeatures[PF_NX_ENABLED] == TRUE) &&
(ExceptionRecord->ExceptionInformation [0] == EXCEPTION_EXECUTE_FAULT)) {
if (((KeFeatureBits & KF_GLOBAL_32BIT_EXECUTE) != 0) ||
(PsGetCurrentProcess()->Pcb.Flags.ExecuteEnable != 0) || // 启用执行属性
(((KeFeatureBits & KF_GLOBAL_32BIT_NOEXECUTE) == 0) &&
(PsGetCurrentProcess()->Pcb.Flags.ExecuteDisable == 0))) { // 禁用执行属性
ExceptionRecord->ExceptionInformation [0] = 0; // 设置附加参数
}
}
}
break;
}
//
// Select the method of handling the exception based on the previous mode.
//
ASSERT ((
!((PreviousMode == KernelMode) &&
(ContextFrame.EFlags & EFLAGS_V86_MASK))
));
// 如果异常是在内核模式下被触发
if (PreviousMode == KernelMode) {
//
// Previous mode was kernel.
//
// If the kernel debugger is active, then give the kernel debugger the
// first chance to handle the exception. If the kernel debugger handles
// the exception, then continue execution. Else attempt to dispatch the
// exception to a frame based handler. If a frame based handler handles
// the exception, then continue execution.
//
// If a frame based handler does not handle the exception,
// give the kernel debugger a second chance, if it's present.
//
// If the exception is still unhandled, call KeBugCheck().
//
// 先判断是否是第一次触发
if (FirstChance == TRUE) {
// 调用KiDebugRoutine函数指针(实际就是内核调试器,如果没有内核调试器,
// 函数指针保存的是KdpTrap函数的地址,如果有,则保存KdpStub函数地址)
if ((KiDebugRoutine != NULL) &&
(((KiDebugRoutine) (TrapFrame, // 线程环境
ExceptionFrame, // 异常栈帧
ExceptionRecord, // 异常信息记录
&ContextFrame, //
PreviousMode,
FALSE)) != FALSE)) {
// 如果调试处理了异常, 则跳转到函数末尾.
goto Handled1;
}
// 没有调试器, 或者有调试器但没有处理得了异常
// 则将异常交给内核得SEH来处理
if (RtlDispatchException(ExceptionRecord, &ContextFrame) == TRUE) {
// 如果SEH处理成功, 则跳转到函数末尾
goto Handled1;
}
}
// 如果调试器和SEH异常处理都没有处理得了异常, 则进行第二次异常分发:
// This is the second chance to handle the exception.
//
// 判断有无内核调试器,并调用(再给内核调试器一次处理异常得机会)
if ((KiDebugRoutine != NULL) &&
(((KiDebugRoutine) (TrapFrame,
ExceptionFrame,
ExceptionRecord,
&ContextFrame,
PreviousMode,
TRUE)) != FALSE)) {
goto Handled1;
}
// 没有内核调试器, 或内核调试器不处理, 则调用函数KeBugCheckEx记录异常,之后蓝屏死机
KeBugCheckEx(
KERNEL_MODE_EXCEPTION_NOT_HANDLED,
ExceptionRecord->ExceptionCode,
(ULONG)ExceptionRecord->ExceptionAddress,
(ULONG)TrapFrame,
0);
}
else // 异常在在用户模式下触发
{
//
// Previous mode was user.
//
// If this is the first chance and the current process has a debugger
// port, then send a message to the debugger port and wait for a reply.
// 如果异常是第一次分发并且进程具有调试端口(被调试状态), 则发送一个消息
// 到调试端口,并等待回复.
// If the debugger handles the exception, then continue execution. Else
// 如果调试器处理了这个异常, 则结束异常的分发. 否则,
// transfer the exception information to the user stack, transition to
// 将异常信息拷贝到用户态的栈中, 并转到
// user mode, and attempt to dispatch the exception to a frame based
// 用户模式 , 在用户模式下尝试将异常派发给异常处理程序.
// handler. If a frame based handler handles the exception, then continue
// 如果异常处理程序处理了异常, 则结束异常分发.
// execution with the continue system service. Else execute the
// 如果用户层的异常处理程序处理不了, 则调用NtRaiseException函数
// NtRaiseException system service with FirstChance == FALSE, which
// 主动触发异常, 并将FirstChance设置为TRUE. 这个函数(KiDispatchException)
// will call this routine a second time to process the exception.
// 将会被第二次调用以继续处理异常.
// If this is the second chance and the current process has a debugger
// 如果这次处理是第二次异常处理,并且进程有一个调试
// port, then send a message to the debugger port and wait for a reply.
// 端口, 则发送一个消息到调试端口,并等待调试器回复.
// If the debugger handles the exception, then continue execution. Else
// 如果调试器回复已经处理了异常, 则结束异常分发. 否则
// if the current process has a subsystem port, then send a message to
// 如果当前触发异常的进程有子系统端口,则发送一个消息到
// the subsystem port and wait for a reply. If the subsystem handles the
// 子系统端口,并等其回复. 若子系统处理了
// exception, then continue execution. Else terminate the process.
// 异常, 则异常分发结束, 否则直接结束掉当前进程.
// If the current process is a wow64 process, an alignment fault has
// occurred, and the AC bit is set in EFLAGS, then clear AC in EFLAGS
// and continue execution. Otherwise, attempt to resolve the exception.
//
//
if (FirstChance == TRUE) {
//
// This is the first chance to handle the exception.
//
// 检查是进程是否被调试,如果当前有内核调试器, 并且进程没有被调试,则将异常交给
// 内核调试器去处理.
if ((KiDebugRoutine != NULL) &&
((PsGetCurrentProcess()->DebugPort == NULL &&
!KdIgnoreUmExceptions) ||
(KdIsThisAKdTrap(ExceptionRecord, &ContextFrame, UserMode)))) {
//
// Now dispatch the fault to the kernel debugger.
//
// 将异常信息交给内核调试器处理
if ((((KiDebugRoutine) (TrapFrame,
ExceptionFrame,
ExceptionRecord,
&ContextFrame,
PreviousMode,
FALSE)) != FALSE)) {
// 处理成功则异常分发结束
goto Handled1;
}
}
// 将异常交给调试子系统去处理. DbgkForwardException函数会将
// 异常记录发送给3环的调试器进程, 并等待3环的调试器回复.
// 如果调试器回复了异常被处理, 则异常分发到此结束.
if (DbgkForwardException(ExceptionRecord, TRUE, FALSE)) {
goto Handled2;
}
// 如果没有用户调试器,或用户调试器没有处理异常则接着往下走.
// 函数会试图将异常记录, 线程环境
//
// Transfer exception information to the user stack, transition
// to user mode, and attempt to dispatch the exception to a frame
// based handler.
ExceptionRecord1.ExceptionCode = 0; // satisfy no_opt compilation
repeat:
try {
//
// If the SS segment is not 32 bit flat, there is no point
// to dispatch exception to frame based exception handler.
//
if (TrapFrame->HardwareSegSs != (KGDT_R3_DATA | RPL_MASK) ||
TrapFrame->EFlags & EFLAGS_V86_MASK ) {
ExceptionRecord2.ExceptionCode = STATUS_ACCESS_VIOLATION;
ExceptionRecord2.ExceptionFlags = 0;
ExceptionRecord2.NumberParameters = 0;
ExRaiseException(&ExceptionRecord2);
}
//
// Compute length of context record and new aligned user stack
// pointer.
//
// 为将线程上下文块整个结构体拷贝到用户的栈空间, 需要找到栈空间上一个空闲
// 的地址, 此处是计算esp(栈顶位置) - 一个线程上下文块的大小, 也就是相当于
// sub esp , sizeof(CONTEXT)
UserStack1 = (ContextFrame.Esp & ~CONTEXT_ROUND) - CONTEXT_ALIGNED_SIZE;
//
// Probe user stack area for writability and then transfer the
// context record to the user stack.
//
// 将指定地址设置为可写入
ProbeForWrite((PCHAR)UserStack1, CONTEXT_ALIGNED_SIZE, CONTEXT_ALIGN);
// 将线程上下文拷贝到用户的栈空间中.
RtlCopyMemory((PULONG)UserStack1, &ContextFrame, sizeof(CONTEXT));
//
// Compute length of exception record and new aligned stack
// address.
//
// 计算处异常信息结构体的在用户栈空间中的位置, 也是为了将异常信息写入
// 到用户栈空间中.
Length = (sizeof(EXCEPTION_RECORD) - (EXCEPTION_MAXIMUM_PARAMETERS -
ExceptionRecord->NumberParameters) * sizeof(ULONG) +3) &
(~3);
UserStack2 = UserStack1 - Length;
//
// Probe user stack area for writeability and then transfer the
// context record to the user stack area.
// N.B. The probing length is Length+8 because there are two
// arguments need to be pushed to user stack later.
//
// 将地址设置为可写
ProbeForWrite((PCHAR)(UserStack2 - 8), Length + 8, sizeof(ULONG));
// 将异常信息拷贝用户的栈空间中.
RtlCopyMemory((PULONG)UserStack2, ExceptionRecord, Length);
//
// Push address of exception record, context record to the
// user stack. They are the two parameters required by
// _KiUserExceptionDispatch.
//
// 构造处一个EXCEPTION_POINTERS的结构体, 并保存线程上下文,异常信息两个结构体
// 变量的首地址.
*(PULONG)(UserStack2 - sizeof(ULONG)) = UserStack1;
*(PULONG)(UserStack2 - 2*sizeof(ULONG)) = UserStack2;
//
// Set new stack pointer to the trap frame.
//
KiSegSsToTrapFrame(TrapFrame, KGDT_R3_DATA);
KiEspToTrapFrame(TrapFrame, (UserStack2 - sizeof(ULONG)*2));
//
// Force correct R3 selectors into TrapFrame.
//
// 设置段选择子
TrapFrame->SegCs = SANITIZE_SEG(KGDT_R3_CODE, PreviousMode);
TrapFrame->SegDs = SANITIZE_SEG(KGDT_R3_DATA, PreviousMode);
TrapFrame->SegEs = SANITIZE_SEG(KGDT_R3_DATA, PreviousMode);
TrapFrame->SegFs = SANITIZE_SEG(KGDT_R3_TEB, PreviousMode);
TrapFrame->SegGs = 0;
//
// Set the address of the exception routine that will call the
// exception dispatcher and then return to the trap handler.
// The trap handler will restore the exception and trap frame
// context and continue execution in the routine that will
// call the exception dispatcher.
//
// 将发生异常的线程的eip的地址设置为KeUserExceptionDispatcher函数的地址
// 这个函数是ntdll中的导出函数,这个导出函数就是负责用户层的异常分发的,
// 在这个函数中,它会把异常发给进程的异常处理机制(VEH,SEH)去处理.
// 这样一来, 当执行流从0环回到3环的时候, eip指向何处, 那个地方的代码就开始
// 被执行.
TrapFrame->Eip = (ULONG)KeUserExceptionDispatcher;
return;
} except (KiCopyInformation(&ExceptionRecord1,
(GetExceptionInformation())->ExceptionRecord)) {
//
// If the exception is a stack overflow, then attempt
// to raise the stack overflow exception. Otherwise,
// the user's stack is not accessible, or is misaligned,
// and second chance processing is performed.
//
if (ExceptionRecord1.ExceptionCode == STATUS_STACK_OVERFLOW) {
ExceptionRecord1.ExceptionAddress = ExceptionRecord->ExceptionAddress;
RtlCopyMemory((PVOID)ExceptionRecord,
&ExceptionRecord1, sizeof(EXCEPTION_RECORD));
goto repeat;
}
}
}
//
// This is the second chance to handle the exception.
//
if (DbgkForwardException(ExceptionRecord, TRUE, TRUE)) {
goto Handled2;
} else if (DbgkForwardException(ExceptionRecord, FALSE, TRUE)) {
goto Handled2;
} else {
ZwTerminateProcess(NtCurrentProcess(), ExceptionRecord->ExceptionCode);
KeBugCheckEx(
KERNEL_MODE_EXCEPTION_NOT_HANDLED,
ExceptionRecord->ExceptionCode,
(ULONG)ExceptionRecord->ExceptionAddress,
(ULONG)TrapFrame,
0);
}
}
//
// Move machine state from context frame to trap and exception frames and
// then return to continue execution with the restored state.
//
Handled1:
// 将异常栈帧, 线程环境设置到线程中.
KeContextToKframes(TrapFrame, ExceptionFrame, &ContextFrame,
ContextFrame.ContextFlags, PreviousMode);
//
// Exception was handled by the debugger or the associated subsystem
// and state was modified, if necessary, using the get state and set
// state capabilities. Therefore the context frame does not need to
// be transferred to the trap and exception frames.
//
Handled2:
return;
}
内核层异常处理
1. 首先尝试让调试器处理异常。若调试器成功处理,程序便继续执行;若调试器无法处理,异常会继续向下传递。
2. 接着由 `RtlDispatchException` 函数来处理异常。要是处理成功,异常处理流程就结束;若未能处理,异常会接着往下传。
3. 之后再次尝试让内核调试器处理异常。如果内核调试器处理了,程序继续执行;若内核调试器处理不了,系统就会崩溃,表现为直接蓝屏。
用户层异常处理
第一次分发
1. 判断用户调试器存在与否:通过检查进程内核对象里的 `DebugPort` 字段,来判断是否存在用户调试器。
2. 无用户调试器时的处理:若没有用户调试器,就把异常交给内核调试器。如果内核调试器处理了异常,程序继续执行;若未处理,异常继续向下传递。
3. 有用户调试器时的处理:将异常发送给用户层调试器。若用户层调试器处理了,程序继续执行;若未处理,异常继续向下传递。
4. 设置EIP并返回:设置指令指针 `EIP` 为 `KeUserExceptionDispatcher` (在用户层此函数名为 `KiUserExceptionDispatcher`),然后返回用户层,此时用户层就会执行 `KiUserExceptionDispatcher` 函数。
5. `KiUserExceptionDispatcher` 的处理流程:`KiUserExceptionDispatcher` 函数会调用 `RtlDispathchException` 函数,该函数依次调用向量化异常处理(`VEH`)、结构化异常处理(`SEH`)以及向量化继续处理(`VCH`)。如果这些处理机制都没有处理异常,程序就会调用 `RaiseException` 发起第二次异常。若有处理成功的情况,就调用 `ZwContinue` 恢复程序继续执行。
第二次分发
当用户层自身无法处理异常,调用 `RaiseException` 产生第二次分发时,异常会交给用户调试器处理。要是用户调试器处理不了,程序就会直接结束。
五、代码探究:伪代码分析用户层KiUserExceptionDispatcher
代码参考WRK-v1.2
// 函数 KiUserExceptionDispatcher 用于处理用户异常
// pExcptRec 指向异常记录结构体,包含了异常的相关信息
// pContext 指向上下文结构体,包含了处理器的寄存器等上下文信息
VOID KiUserExceptionDispatcher(PEXCEPTION_RECORD pExcptRec, CONTEXT* pContext)
{
DWORD dwRetValue;
// 尝试通过 RtlDispatchException 函数分发异常
// 如果成功分发异常(返回值为非零)
if (RtlDispatchException(pExcptRec, pContext))
{
// 调用 NtContinue 函数,尝试继续执行上下文,第二个参数为 0
dwRetValue = NtContinue(pContext, 0);
}
else
{
// 如果异常分发失败,调用 NtRaiseException 函数再次引发异常
// 传入异常记录、上下文和 0(可能是某些标志或参数)
dwRetValue = NtRaiseException(pExcptRec, pContext, 0);
}
// 定义一个新的异常记录结构体 excptRec2
EXCEPTION_RECORD excptRec2;
// 将之前操作得到的返回值设置为新异常记录的异常代码
excptRec2.ExceptionCode = dwRetValue;
// 设置异常标志为不可继续,意味着这个异常不能被简单地继续执行来恢复
excptRec2.ExceptionFlags = EXCEPTION_NONCONTINUABLE;
// 将原始的异常记录指针赋值给新异常记录的 ExceptionRecord 成员
excptRec2.ExceptionRecord = pExcptRec;
// 设置异常参数的数量为 0
excptRec2.NumberParameters = 0;
// 调用 RtlRaiseException 函数引发新构建的异常记录所表示的异常
RtlRaiseException(&excptRec2);
}
`KiUserExceptionDispatcher` 函数在Windows系统中用于用户层的异常分发,其主要作用如下:
- 准备异常处理环境:当从内核模式回到用户模式处理异常时,它负责将相关的异常信息和上下文准备好,使得用户层的异常处理机制能够正确地处理异常。例如,它会让 `esp` 指向包含异常信息的 `EXCEPTION_POINTERS` 变量的地址,让 `eip` 指向自身,以便开始执行用户层的异常分发流程。
- 触发异常处理流程:在准备好环境后,该函数会触发用户层的异常处理流程,通过遍历用户层的异常处理链,如先遍历向量化异常处理(VEH)并调用相应的异常处理函数,再遍历结构化异常处理(SEH)并调用其异常处理函数,让系统能够根据具体的异常情况进行相应的处理,例如尝试恢复程序执行、进行错误提示或采取其他合适的措施来处理异常情况,以保证程序的稳定性和可靠性。