异常的分类
(1)软件异常:由操作系统 / 应用程序 引发
用户:RaiseException -> RltRaiseException -> NtRaiseException -> KiRaiseException
内核:RtlRaiseException -> NtRaiseException -> KiRaiseException
软件异常归根结底都是基于 RaiseException 这个用户态 API 和 NtRaiseException 的内核服务建立起来的。
void RaiseException(
DWORD dwExceptionCode , // 异常状态码
DWORD dwExceptionFlags, // 异常标志(可持续 / 不可持续)
DWORD nNumberofArguments, // lpArguments[] 数组长度
const DWORD* lpArguments // 传递给异常处理程序的筛选表达式
);
// 具体作用就是将异常信息传递给 EXCEPTION_RECORD 这个结构,然后再调用 RtlRaiseException 函数
/*
typedef struct _EXCEPTION_RECORD {
DWORD ExceptionCode; // 异常状态码
DWORD ExceptionFlags; // 异常标志
struct _EXCEPTION_RECORD *ExceptionRecord; // 指向下一个异常的指针
PVOID ExceptionAddress; // 保存异常发生的地址
DWORD NumberParameters; // ExceptionInformation[] 数组参数个数
ULONG_PTR ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS]; // 异常描述信息
} EXCEPTION_RECORD;
*/
(2)硬件异常:由 CPU 引发
① 错误异常
处理错误异常时,操作系统首先保存当前环境(比如寄存器的值),然后调用相应的异常处理函数,如果异常处理成功则恢复现场环境继续执行。
② 陷阱异常
和错误异常不同,陷阱异常发生时会保存要执行的下一条指令的环境(而不是正在执行的指令的环境),调试器的断点就是基于陷阱异常实现的。
③ 终止异常
·主要用来处理严重的硬件错误,和上面的异常不同,这种异常不会恢复执行而是直接退出。
异常和中断的关系
中断可以再任何时候发生,和 CPU 正在执行什么指令无关,可以被取消。而异常是由于 CPU 执行了某条指令引起的,不能被取消。
异常的分发处理
异常产生时,CPU是通过IDT进入内核来寻找处理函数:
(1)内核可以处理这个异常
异常处理程序执行完后会恢复现场并继续执行,这个过程我们感知不到。
(2)内核不能处理这个异常
① 如果这个异常来自内核,蓝屏。
② 如果这个异常来自应用程序,则异常处理权转交给应用程序的异常处理函数。如果程序处理了异常则程序继续执行,如果没有处理这个异常则程序崩溃。
上面的 ② 有两种可能:应用程序被调试 / 没有被调试 ——
<1> 如果程序被调试,则异常处理权限转交给调试器(应用程序将调试信息包装成调试事件并发送给调试器,调试器使用 WaitForDebugEvent 获得该调试事件),调试器经过一系列操作后调用 ContinueDebugEvent 继续执行程序。如果调试器处理了这个异常则程序继续执行,如果没有处理则将异常处理权限转交给应用程序(也就是 <2> )。
<2> 如果程序没有被调试,则将异常信息连同线程上下文环境送入程序的栈中,并调用 KiUserExceptionDisptcher (由 ntdll 导出,所有的异常分发都会走这个函数)但KiUserExceptionDisptcher 实际不执行什么功能,实际发挥作用的是 RtlExceptionDisptcher,之后如果 RtlExceptionDisptcher 返回成功则调用 ZwContinue 继续执行,否则调用 ZwRaiseException 结束进程(如果正在被调试的话就把异常再次抛给调试器)。
但是如果调试器还是没有处理这个异常呢?(是个狠人🙄)难道又要交给程序了吗(这样不就陷入一个死循环吗)?
但操作系统不傻,这时候操作系统会调用 ExceptionPort 通知 csrss.exe 弹出一个对话框:
csrss.exe 是 Windows 子系统的灵魂,它监管着系统内运行着的所有Windows进程和线程,每个进程在创建后都要到它这里注册登记后方能运行,退出时也要到此报告注销。除了掌管着各个进程的“生死存亡”,CSRSS在桌面管理、终端登录、控制台管理、HardError报告、和DOS虚拟机等方面也起着重要作用。如果尝试强行杀死CSRSS进程,系统便会蓝屏。
RtlExceptionDisptcher 的具体工作如下:
首先遍历 VEH 链表,逐个执行异常处理器,一旦某个处理器处理成功则返回成功,线程继续运行。如果 VEH 链表遍历完毕异常仍然没有被处理则遍历 SEH 链表,再逐个执行 SEH 的异常处理器一旦某个处理器处理成功则返回成功,线程继续运行。
如果 VEH 和 SEH 都没有处理这个异常,则进程结束。
硬件异常会通过IDT去调用异常处理例程(一般为KiTrap系列函数)。
软件异常则是通过API的层层调用传递异常的信息。
但无论是硬件异常还是软件异常,最后都会走到 KiDispatchException:
VEH(向量化异常处理)
在每个进程的 ntdll.dll 中,有一张 VEH 链表,可以向其中添加一个节点来注册自己的异常处理器(通过调用 AddVectordExceptionHandler):
PVOID AddVectoredExceptionHandler(
ULONG First, // 0则把处理器放在链表尾,否则把处理器放在链表头
PVECTORED_EXCEPTION_HANDLER Handler // 自己定义的异常处理函数指针
);
// 注意:Handler 是进程级的,也就是说进程内的所有异常都会调用 Handler 指向的函数。
SEH(结构化异常处理)
SEH是基于线程的一种处理机制,依赖于栈进行存储和查找,所以也被称作是基于栈帧的异常处理机制。(所以就是说它只能处理自己线程的异常,而不是像 VEH 那样可以影响整个进程)
SEH 其实姐可以理解为高级语言中的 __try{} __except(){}
(或 try , catch),它的作用就是构造一个 SEH 节点,并将该节点放入 SEH 链表头部。这个节点的结构如下:
SEH 链表的表头指针永远都在 fs:[0]
这个位置。
UEF 和 VCH
TopLevelEH(顶级异常处理)
本质上也是 SEH,在最顶层的SEH中,可以注册一个顶层异常处理器(和 SEH 不同的是它可以影响所有的线程)。
当 SEH 链表中的异常处理器都处理不了某个异常,在最顶层的SEH中就会检查是否注册了顶层异常处理,如果注册了顶级异常处理器,就会给 SEH “最后一次处理异常的机会” ,把异常抛给 TopLevelEH(但如果程序被调试时就会忽略 TopLevelEH)。
所有异常的必经之路 KiDispatchException
(图片来自网络)
VOID KiDispatchException(
IN PEXCEPTION_RECORD ExceptionRecord, // 描述异常
IN PKEXCEPTION_FRAME ExceptionFrame, // 描述异常发生时的处理器状态,对于x86结构总是NULL
IN PKTRAP_FRAME TrapFrame, // 陷阱帧,异常发生时处理器自动记录的当前处理器的状态
IN KPROCESSOR_MODE PreviousMode, // 触发异常代码的执行模式是用户模式还是内核模式
IN BOOLEAN FirstChance // 否是第一轮分发这个异常(对于一个异常,Windows系统会最多分发两轮)
)
/*
typedef struct _EXCEPTION_RECORD {
DWORD ExceptionCode; // 异常发生的原因
DWORD ExceptionFlags; // 是否为连续异常
struct _EXCEPTION_RECORD *ExceptionRecord; // 当发生嵌套异常时提供附加信息
PVOID ExceptionAddress; // 异常发生的地址
DWORD NumberParameters; // 与异常关联的参数数目(最后一个参数的数组长度)
ULONG_PTR ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS]; // 描述异常的附加参数数组
} EXCEPTION_RECORD;
*/
/*
typedef struct _KTRAP_FRAME
{
ULONG DbgEbp;
ULONG DbgEip;
ULONG DbgArgMark;
ULONG DbgArgPointer;
WORD TempSegCs;
UCHAR Logging;
UCHAR Reserved;
ULONG TempEsp;
ULONG Dr0;
ULONG Dr1;
ULONG Dr2;
ULONG Dr3;
ULONG Dr6;
ULONG Dr7;
ULONG SegGs;
ULONG SegEs;
ULONG SegDs;
ULONG Edx;
ULONG Ecx;
ULONG Eax;
ULONG PreviousPreviousMode;
PEXCEPTION_REGISTRATION_RECORD ExceptionList;
ULONG SegFs;
ULONG Edi;
ULONG Esi;
ULONG Ebx;
ULONG Ebp;
ULONG ErrCode;
ULONG Eip;
ULONG SegCs;
ULONG EFlags;
ULONG HardwareEsp;
ULONG HardwareSegSs;
ULONG V86Es;
ULONG V86Ds;
ULONG V86Fs;
ULONG V86Gs;
} KTRAP_FRAME, *PKTRAP_FRAME;
*/
分发过程:
1)将 TrapFrame 参数备份到 Context 结构中
2)判断是内核模式(0)还是应用模式(1)
3)判断是否是第一次调用
4)判断是否有内核调试器(有的话交给调试器)
5)调用 RtlDispatchException 处理异常
保存 FS:0(KPCR -> ExceptionList),它指向一个结构:
typedef struct _EXCEPTION_REGISTRATION_RECORD{
struct _EXCEPTION_REGISTRATION_RECORD *Next; // 指向下一个_EXCEPTION_REGISTRATION_RECORD
PEXCEPTION_ROUTINE Handler; // 异常处理函数
} EXCEPTION_REGISTRATION_RECORD;
RtlDispatchException 的作用就是遍历这个链表,查找对应的异常处理函数。
6)RtlDispatchException 成功则正常执行,失败则再次判断有没有内核调试器,有则通知内核调试器,没有则蓝屏
内核态的异常处理
内核模式则查 IDT 表找到对应的异常处理函数
用户态的异常处理
调用 DbgkForwardException(将异常信息包装后发送给 r3 ):
- 检擦调试端口(DebugPort)是否为空
- 调用 DbgkSendApiMessage 将异常发送给 r3 调试器
- 若 r3 无调试器或调试器处理不了此异常,则调用 KiUserExceptionDispatcher
对于 KiUserExceptionDispatcher 而言,主要功能就是调用 RtIDispatchException 遍历 VEH 和 SEH,成功则调用 ZwContinue 进入 r0 ,失败则调用 ZwRaiseException 进行第二轮(最后一次机会)异常分发。
如果第二轮异常分发仍然失败,KidispatchException 就会终止当前线程。