目录

Win异常体系分析(1):内核中异常派发流程

CPU异常、IDT表

  1. 当CPU发生异常(例如:除0异常)时会发出中断,到IDT表 根据中断向量号查询对应的ISR中断处理回调
  2. 在IDA中Alt + T 搜索字符串_IDT,定位到IDT表

/exceptiondispatch/Image/1-IDT%E8%A1%A8.png

KiTrap00函数

  1. 已知IDT表中的0号异常为除零异常,如图所示:

/exceptiondispatch/Image/2-%E9%99%A4%E9%9B%B6%E5%BC%82%E5%B8%B8%E6%8E%A5%E7%AE%A1%E5%87%BD%E6%95%B0.png

  1. 在中断处理函数中,并没有直接处理异常,微软在设计异常处理体系时,是希望程序员自己优先处理异常。该函数核心调用点为CommonDispatchException 函数

/exceptiondispatch/Image/3-%E9%99%A4%E9%9B%B6%E5%BC%82%E5%B8%B8%E6%8E%A5%E7%AE%A1%E5%87%BD%E6%95%B0.png

CommonDispatchException函数

  1. 这个函数头部抬升堆栈,分配了一个局部变量ExceptRecord对象
  2. 填充ExceptRecord对象的各个字段内容,该过程就是异常的记录
  3. 最后调用KiDipatchException ,异常的分发

/exceptiondispatch/Image/4-CommonDispatchException.png

模拟异常

  1. 代码如下所示,主动抛出一个异常:

/exceptiondispatch/Image/5-%E6%A8%A1%E6%8B%9F%E5%BC%82%E5%B8%B8.png

  1. 该代码的反汇编如下所示,很明显第一个参数push 1,然后call __CxxThrowException,这层属于编译器拓展,不用深入分析

/exceptiondispatch/Image/6-%E6%A8%A1%E6%8B%9F%E5%BC%82%E5%B8%B8.png

  1. 继续跟进这个函数,该函数内部调用了RaiseException,该函数是R3模拟异常起始点

/exceptiondispatch/Image/7-%E6%A8%A1%E6%8B%9F%E5%BC%82%E5%B8%B8.png

RaiseException

  1. 在这个函数会在堆栈中构造ExceptionRecord对象,完成异常的记录
  2. 在模拟异常中,ExceptionAddress是RaiseException函数地址,并不是原代码中throw 1代码地址

/exceptiondispatch/Image/8-%E6%A8%A1%E6%8B%9F%E5%BC%82%E5%B8%B8.png

KiRaiseException

  1. 后续代码执行流程:
RaiseException 
	-> RtlRaiseException 
		-> ntdll!NtRaiseException 
			-> nt!NtRaiseException 
				-> KiRaiseException 
					-> KiDispatchException

CPU异常、模拟异常小结

  1. CPU异常:CPU异常发生时,一定会到IDT表中根据异常向量号查询对应的异常处理回调

换句话说:想知道哪些属于CPU异常,直接查IDT表即可0

  1. 模拟异常:依赖编译器拓展,主动调用Kernel32!RaiseException抛出异常
  2. 最后的共同点都是调用KiDispatchException 函数实现异常派发(总的分发点)

/exceptiondispatch/Image/9-%E5%BC%82%E5%B8%B8%E5%B0%8F%E7%BB%93.png

KiDispatchException(内核异常派发核心)

  1. 该函数签名如下所示:
VOID KiDispatchException(
   IN PEXCEPTION_RECORD ExceptionRecord,    // 异常记录
   IN PKEXCEPTION_FRAME ExceptionFrame,     // 异常帧,不用管,都是NULL
   IN PKTRAP_FRAME TrapFrame,            	// R3最后现场
   IN KPROCESSOR_MODE PreviousMode,    		// 先前模式
   IN BOOLEAN FirstChance    				// 是否第一次分发
);

KeContextFromKframes

  1. 在KiExceptionDispatch 函数开始,首先调用KeContextFromKframes 将TrapFrame备份到Context

/exceptiondispatch/Image/10-TrapFrame%E8%BD%ACContext.png

内核调试器接管

  1. 判断PreviousMode 是否为内核模式,如果不成立就跳走
  2. 再判断FirstChance 是否第一次,不成立则跳走
  3. 再判断_KiDebugRoutine 是否为空,成立则跳走
  4. _KiDebugRoutine 不为空,说明此时挂着内核调试器,将本次异常分发给内核调试器

/exceptiondispatch/Image/11-KiDispatchException.png

  1. 继续看汇编:没有内核调试器、内核调试器没有处理该异常(即返回0),最后都会跳到同一处代码段loc_438EB6
  2. 如果内核调试顺利处理了异常,就会调用KeContextToKframes 将Context恢复到TrapFrame,KiDispatchException 正常返回,R0恢复执行

/exceptiondispatch/Image/12-KiDispatchException.png

内核调试不接管

  1. 不存在内核调试器、或者内核调试器没处理时,都会跳转到loc_438EB6 执行,这部分代码如下:
  2. 将ExceptionRecord、Context压入堆栈,调用RtlDispatchException 函数

/exceptiondispatch/Image/13-KiDispatchException.png

nt!RtlDispatchException

  1. 这个函数是负责异常的分发,内核调试器不处理的话,只剩下内核SEH了
  2. 该函数内部主要调用RtlpGetRegistrationHead 函数,该函数会去获取当前线程SEH异常链表头

/exceptiondispatch/Image/14-KiDispatchException.png

小结

  1. 分析了以上异常体系,可以知道用户异常发生时的接管优先级:R0调试器 > R3调试器 > VEH > SEH(VEH、SEH在后续章节中会展开)
  2. KiDispatchException这个函数,可以是说异常体系的战略要地,所有的异常分发都要经过它,想办法Hook这个函数,就能接管所有的异常
异常体系分析 Windows内核