漏洞分析——Windows异常处理机制 这世上谁还不是摸着石头过河

Windows在正常启动之后,运行在保护模式之下,这个时候如果发生中断或者异常事件的时候,CPU就会通过IDT(中断描述符表)来寻找处理函数,也因此,IDT是CPU和操作系统交接中断和异常的接口

IDT

IDT的位置和长度是由CPU的IDTR寄存器描述的,IDTR寄存器的长度是48位,这其中高32位是表基值,低16位是表的长度。IDT的每一项都是一个门结构,是发生中断或者异常的时候CPU的交接控制权的“要塞”。
IDT的门结构可以分为以下3种门描述符
1:任务门描述符,用来描述CPU的任务切换
2:中断门描述符:用来描述中断处理程序的入口
3:陷阱门描述符:用来描述异常处理程序的入口
在系统触发异常之后,进行异常处理程序执行之前,还有一步最重要的事情需要处理:异常处理的准备工作

异常处理的准备工作

在进行异常处理的时候,会通过中断类型号(这里将异常也当做中断处理)去寻找对应的中断处理程序,当然对于异常来说,这里指的是陷阱门,当异常发生的时候,会根据IDT表中对应的中断类型号去寻找对应的中断处理程序,找到之后去执行对应的程序,但是在此之前,系统通常会奖异常信息进行包装以防备在之后恢复现场的时候使用。
包装的内容主要有两种,一种是关于异常发生的基本信息,其中包括异常的发生地址、异常信息、异常代码等(这些信息都封装在EXCEPTION_RECORD结构体里边,该结构体的定义如下:

typedef   struct  _EXCEPTION_RECORD
{
	typedef struct _EXCEPTION_RECORD {
  DWORD                    ExceptionCode;  //异常代码
  DWORD                    ExceptionFlags;  //异常标志
  struct _EXCEPTION_RECORD  *ExceptionRecord;  //指向另一个_EXCEPTION_RECORD结构的指针
  PVOID                    ExceptionAddress;//异常发生的地址
  DWORD                    NumberParameters;//下一个元素的数量
  ULONG_PTR                ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];//附加信息
} EXCEPTION_RECORD, *PEXCEPTION_RECORD;


另一种包装的信息是陷阱帧,这些信息详细的记录了在异常发生的时候系统的各个寄存器的值,但是在实际的应用中,这些值是提供给系统自身的内核或者调试器系统使用,在系统把控制权交给用户自己注册的异常处理程序的时候,通常把这个结构转换为名为CONTEXT的结构,在这个结构里边完整的保存了线程运行时各个主要寄存器的完整镜像,用于保存线程运行环境。

typedef struct _CONTEXT
{
    DWORD           ContextFlags    // 标志位,标识整个结构体里边哪一部分是有效的              +00h
    //当ContextFlags里边包含CONTEXT_DEBUG_REGISTRS以下部分有效
    DWORD           Dr0             //  |               +04h
    DWORD           Dr1             //  |               +08h
    DWORD           Dr2             //  >调试寄存器     +0Ch
    DWORD           Dr3             //  |               +10h
    DWORD           Dr6             //  |               +14h
    DWORD           Dr7             // -|               +18h
	//当ContextFlags里边包含CONTEXT_FLOATING_POINT的时候以下部分有效
    FLOATING_SAVE_AREA FloatSave;   //浮点寄存器区      +1Ch~~~88h
	//当ContextFlags里边包含CONTEXT_SEGMENTS的时候以下部分有效
    DWORD           SegGs           //-|                +8Ch
    DWORD           SegFs           // |\段寄存器       +90h
    DWORD           SegEs           // |/               +94h
    DWORD           SegDs           //-|                +98h
	//当ContextFlags里边包含CONTEXT_INTEGER的时候以下部分有效
    DWORD           Edi             //________          +9Ch
    DWORD           Esi             // |  通用          +A0h
    DWORD           Ebx             // |   寄           +A4h
    DWORD           Edx             // |   存           +A8h
    DWORD           Ecx             // |   器           +ACh
    DWORD           Eax             //_|___组_          +B0h
	//当ContextFlags里边包含CONTEXT_CONTROL的时候以下部分有效
    DWORD           Ebp             //++++++            +B4h
    DWORD           Eip             // |控制            +B8h
    DWORD           SegCs           // |寄存            +BCh
    DWORD           EFlag           // |器组            +C0h
    DWORD           Esp             // |                +C4h
    DWORD           SegSs           //++++++            +C8h
	//当ContextFlags里边有CONTEXT_EXTENDED_REGISTERS以下部分有效
    BYTE    ExtendedRegisters[MAXIMUM_SUPPORTED_EXTENSION];
} CONTEXT;

至此,异常处理的准备工作已经完毕,在此之后(也就是异常信息包装完毕之后),异常处理函数会进一步去处理系统内核的nt!KiDispatchException函数,该函数的函数原型以及各参数如下:

VOID 
KiDispatchException (
    IN PEXCEPTION_RECORD ExceptionRecord,  //异常结构信息
    IN PKEXCEPTION_FRAME ExceptionFrame,
    IN PKTRAP_FRAME TrapFrame,       //发生异常的时候的陷阱帧
    IN KPROCESSOR_MODE PreviousMode,//异常发生在内核模式还是用户模式  
    IN BOOLEAN FirstChance//是否是第一次处理异常
    )

在该函数中,会根据是否存在内核调试器、用户态调试器以及调试器对于异常的干扰结果来完成不同的处理过程
内核态和用户态下,系统的异常处理过程:

内核态下系统的异常分发过程:
当PreviousMode是KernelMode的时候,表明是内核态下的异常事件,KiDispatchException函数会按照以下步骤进行异常的分发:
1:检测当前系统是否存在内核调试器调试,如果内核调试器不存在跳过该步骤。若果存在内核调试器,那么就把控制权交给内核调试器,并且标注是第一次对异常进行处理,之后会根据用户的设置决定是否对该异常作出处理;如果内核调试器无法确定是否要对当前的异常事件进行处理,那么就产生中断,将控制权交给用户由用户最终决定是否对该异常进行处理。如果内核调试器正确处理了该异常事件,那么程序将会返回发生异常的地方继续执行。
2:如果没有内核调试器或者在第一次将控制权交给调试器的时候选择不对该异常事件进行处理,那么系统就会调用nt!RtlDispatchException函数,根据线程注册的SEH(结构化异常处理)进行处理。
3:如果说nt!RtlDispatchException没有处理该异常,系统会将控制权再次交回内核调试器(也就是给内核调试器第二次处理异常的机会)
4:如果不存在内核调试器,或者说在第二次获得控制权的时候内核调试器还是选择不处理该异常,系统就会直接调用KeBugCheckEx来产生蓝屏错误

用户态下的异常分发过程
当PreviousMode为UserMode的时候,说明该异常是用户态下的异常事件,这个时候的KiDispatchException函数依然会像之前一样进行异常的分发,主要的分发过程如下:
(依然会首先询问是否存在内核态的调试器,如果存在那么将控制权交给内核态的调试器进行处理,但是一般情况下,内核态的调试器对用户态的异常没有多大的好奇心,但是也说明我们可以采用内核态的调试器来调试用户态的异常事件)
1:如果存在用户态的调试器,将控制权和异常信息交给用户态的调试器,同时标注是第一次异常处理;如果不存在用户态调试器,跳过这一步
2:如果不存在用户态的调试器,或者在第一次调试器进行异常处理的时候选择不处理异常事件,那么在栈上放置两个结构:EXCEPTION_RECORD和CONTEXT,同时将控制权返回到用户态的ntdll.dll中的KiUserExceptionDispatcher函数,由该函数调用ntdll!RtlDispatchException函数进行用户态的异常处理(这里涉及到SEH和VEH两种机制,后边会详细阐述)
3:如果ntdll!RtlDispatchException函数在调用用户态的异常处理函数未能正确处理该异常事件,那么异常处理过程会再次返回nt!KiDispatchException,它将再次把异常信息发送给用户态的调试器,如果用户态的调试器不存在,那么不会进行第二次的异常分发,程序直接结束。
4:如果在第二次机会的时候,调试器仍然选择不处理异常事件,那么nt!KiDisptachException会再次尝试把异常分发给进程的异常处理端口,该端口通常由系统进程csrss.exe进行监听。子系统监听到该错误之后,通常会显示一个应用程序错误的对话框,用户可以选择结束进程或者将进程附加到调试器上
5:在终结程序之前,系统会再次调用发生异常的线程中的所有异常处理过程,这是线程异常处理过程获得的清理未释放资源的最后的机会,之后程序终结。
以上就是在用户态和内核态的议程的分发过程,流程图如下:
在这里插入图片描述

后记(写在最后)

等到40岁再相逢,笑说多年来无泪的伤痛
没有哭,只有笑
笑我当年的荒谬
——笑我独自一人走出风中

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值