Windows异常处理

c++

Posted by YiMiTuMi on October 20, 2021

Windows异常处理

异常与调试是紧密相连的,异常是调试的基础。

异常产生后,首先是要记录异常信息(异常的类型、异常发生的位置等),然后要寻找异常的处理函数,我们称为异常的分发,最后找到异常处理函数并调用,我们称为异常处理

异常调用流程:

记录异常信息 → 异常的分发 → 异常处理

异常的分类:

1)CPU产生的异常(如除零)

2)软件模拟产生的异常

异常的分发与处理:

异常可以发生在用户空间,也可以发生在内核空间。

无论是CPU异常还是模拟异常,是用户层异常还是内核异常,都要通过 KiDispatchException 函数进行分发。

异常结构体 _EXCEPTION_RECORD

结构:

kd> dt _EXCEPTION_RECORD
ntdll!_EXCEPTION_RECORD
   +0x000 ExceptionCode    : Int4B                    //异常代码,异常类型
   +0x004 ExceptionFlags   : Uint4B                   //异常状态
   +0x008 ExceptionRecord  : Ptr32 _EXCEPTION_RECORD  //下一个异常 (只有出现嵌套异常时,才会使用)
   +0x00c ExceptionAddress : Ptr32 Void               //异常发生地址      
   +0x010 NumberParameters : Uint4B                   //附加参数个数              
   +0x014 ExceptionInformation : [15] Uint4B

_EXCEPTION_RECORD结构体的主要作用是用来记录异常信息。

** +0x004 ExceptionFlags : Uint4B:**

用来标识异常状态:

ExceptionFlags = 0 //CPU产生的异常 

ExceptionFlags = 1 //软件模拟产生的异常

ExceptionFlags = 8 //堆栈错误

ExceptionFlags = 0x10 //嵌套异常(在处理异常时,再次出现异常)

+0x008 ExceptionRecord : Ptr32 _EXCEPTION_RECORD:

下一个异常,通常是空的,只有出现嵌套异常时,才会使用。

CPU异常的产生与记录

CPU异常的产生流程:

CPU指令检测到异常 (如除零)
		↓
查IDT表,执行中断处理函数
		↓
CommonDispatchException (构建_EXCEPTION_RECORD)
		↓
KiDispatchException     (分发异常:目的是找到异常的处理函数)

除零异常引发零号中断,0号中断处理函数:

 _KiTrap00       proc near               ; DATA XREF: INIT:_IDT↓o

 var_2           = word ptr -2
 arg_4           = dword ptr  8

 ; FUNCTION CHUNK AT .text:004081C7 SIZE 00000021 BYTES

                 push    0
                 mov     [esp+4+var_2], 0
                 push    ebp
                 push    ebx
                 push    esi
                 push    edi
                 push    fs
                 mov     ebx, 30h ; '0'
                 mov     fs, ebx
                 assume fs:nothing
                 mov     ebx, large fs:0
                 push    ebx
                 sub     esp, 4
                 push    eax
                 push    ecx
                 push    edx
                 push    ds
                 push    es
                 push    gs
							 //以上保存环境,以_Trap_Frame结构形式
                 mov     ax, 23h ; '#'
                 sub     esp, 30h
                 mov     ds, eax
                 assume ds:nothing
                 mov     es, eax
                 assume es:nothing
                 mov     ebp, esp
                 test    [esp+68h+arg_4], 20000h
                 jnz     short V86_kit0_a

 loc_40838F:                             ; CODE XREF: V86_kit0_a+25↑j
                 cld
                 mov     ebx, [ebp+60h]
                 mov     edi, [ebp+68h]
                 mov     [ebp+0Ch], edx
                 mov     dword ptr [ebp+8], 0BADB0D00h
                 mov     [ebp+0], ebx
                 mov     [ebp+4], edi
                 test    byte ptr ds:0FFDFF050h, 0FFh
                 jnz     Dr_kit0_a

 loc_4083B3:                             ; CODE XREF: Dr_kit0_a+10↑j
                                         ; Dr_kit0_a+7C↑j
                 test    dword ptr [ebp+70h], 20000h
                 jnz     short loc_4083F8
                 test    byte ptr [ebp+6Ch], 1
                 jz      short loc_4083C9
                 cmp     word ptr [ebp+6Ch], 1Bh
                 jnz     short loc_4083E6

 loc_4083C9:                             ; CODE XREF: _KiTrap00+70↑j
                 sti
                 push    ebp
                 call    _Ki386CheckDivideByZeroTrap@4 ; Ki386CheckDivideByZeroTrap(x)
                 mov     ebx, [ebp+68h]
                 jmp     loc_4081C7
 ; ---------------------------------------------------------------------------

 loc_4083D8:                             ; CODE XREF: _KiTrap00+A6↓j
                                         ; _KiTrap00+B7↓j
                 sti
                 mov     ebx, [ebp+68h]   //_Trap_Frame.eip 即发生异常时,执行的地址
                 mov     eax, 0C0000094h  //操作系统定义的异常类型,可以查到
                 jmp     loc_4081C7       //跳转处理异常
 ; ---------------------------------------------------------------------------

 loc_4083E6:                             ; CODE XREF: _KiTrap00+77↑j
                 mov     ebx, ds:0FFDFF124h
                 mov     ebx, [ebx+44h]
                 cmp     dword ptr [ebx+158h], 0
                 jz      short loc_4083D8

 loc_4083F8:                             ; CODE XREF: _KiTrap00+6A↑j
                 push    0
                 call    _Ki386VdmReflectException_A@4 ; Ki386VdmReflectException_A(x)
                 or      al, al
                 jnz     Kei386EoiHelper@0 ; Kei386EoiHelper()
                 jmp     short loc_4083D8
 _KiTrap00       endp

异常处理函数中,并不会直接处理异常,而是会跳转,去调用 CommonDispatchException 函数处理,之所以这样设置是CPU希望程序员有机会去处理。

 ; START OF FUNCTION CHUNK FOR _KiTrap00
 ;   ADDITIONAL PARENT FUNCTION _KiTrap01
 ;   ADDITIONAL PARENT FUNCTION _KiTrap04
 ;   ADDITIONAL PARENT FUNCTION _KiTrap05
 ;   ADDITIONAL PARENT FUNCTION _KiTrap06
 ;   ADDITIONAL PARENT FUNCTION _KiTrap0D

 loc_4081C7:                             ; CODE XREF: _KiTrap00+83↓j
                                         ; _KiTrap00+91↓j ...
                 xor     ecx, ecx
                 call    CommonDispatchException

 loc_4081CE:                             ; CODE XREF: _KiTrap07+1E5↓j
                                         ; _KiTrap07+200↓j ...
                 xor     edx, edx
                 mov     ecx, 1
                 call    CommonDispatchException

 loc_4081DA:                             ; CODE XREF: _KiTrap07+1EF↓j
                                         ; _KiTrap07+30E↓j ...
                 xor     edx, edx

 loc_4081DC:                             ; CODE XREF: _KiTrap0C+A8↓j
                                         ; _KiTrap0C+109↓j ...
                 mov     ecx, 2
                 call    CommonDispatchException
                 mov     edi, edi
 ; END OF FUNCTION CHUNK FOR _KiTrap00

CommonDispatchException 函数分析

CommonDispatchException 函数构建了_EXCEPTION_RECORD结构体,后调用KiDispatchException函数分发异常。

在CommonDispatchException的函数中:

ebx = _Trap_Frame.eip 即发生异常时,执行的地址
eax = 操作系统定义的异常类型,可以查到
ecx = 附加参数个数  

这3个参数是由,IDT表找到的异常函数中调用 CommonDispatchException 时传值过来的。

 CommonDispatchException proc near       ; CODE XREF: _KiTrap00-187↑p
                                         ; _KiTrap00-17B↑p ...

 var_50          = dword ptr -50h
 var_4C          = dword ptr -4Ch
 var_48          = dword ptr -48h
 var_44          = dword ptr -44h
 var_40          = dword ptr -40h
 var_3C          = byte ptr -3Ch

                 sub     esp, 50h                   //提升栈顶,分配50h的临时空间用来保存_EXCEPTION_RECORD结构体
                 mov     [esp+50h+var_50], eax      //_EXCEPTION_RECORD.ExceptionCode = eax = 操作系统定义的异常类型,可以查到
                 xor     eax, eax                   //清零 eax = 0
                 mov     [esp+50h+var_4C], eax      // _EXCEPTION_RECORD.ExceptionFlags = 0,  CPU产生的异常 
                 mov     [esp+50h+var_48], eax      // _EXCEPTION_RECORD.ExceptionRecord = 0
                 mov     [esp+50h+var_44], ebx      // _EXCEPTION_RECORD.ExceptionAddress = ebx = _Trap_Frame.eip 即发生异常时,执行的地址
                 mov     [esp+50h+var_40], ecx      // _EXCEPTION_RECORD.NumberParameters = ecx  附加参数个数

                 cmp     ecx, 0
                 jz      short loc_408211           //ecx == 0, 跳转
				 
				 //ecx不等于0,则保存参数
                 lea     ebx, [esp+50h+var_3C]
                 mov     [ebx], edx
                 mov     [ebx+4], esi
                 mov     [ebx+8], edi

 loc_408211:                             ; CODE XREF: CommonDispatchException+1B↑j
                 mov     ecx, esp        //此时 ecx 保存结构体地址
                 test    dword ptr [ebp+70h], 20000h
                 jz      short loc_408223
                 mov     eax, 0FFFFh
                 jmp     short loc_408226
 ; ---------------------------------------------------------------------------

 loc_408223:                             ; CODE XREF: CommonDispatchException+32↑j
                 mov     eax, [ebp+6Ch]

 loc_408226:                             ; CODE XREF: CommonDispatchException+39↑j
                 and     eax, 1
										 //传参数调用 KiDispatchException 函数
                 push    1               ; char
                 push    eax             ; int
                 push    ebp             ; BugCheckParameter3
                 push    0               ; int
                 push    ecx             ; ExceptionRecord
                 call    _KiDispatchException@20 ; KiDispatchException(x,x,x,x,x)
                 mov     esp, ebp
                 jmp     Kei386EoiHelper@0 ; Kei386EoiHelper()
 CommonDispatchException endp

模拟异常的产生与记录

模拟异常的函数调用流程:

CxxThrowException   //每个语言可能不一样
		↓
(Kernel32.dll) RaiseException
		↓
ntdll.dll !RtlRaiseException
		↓
ntdll.dll NT!NtRaiseException
		↓
ntdll.dll NT!KiRaiseException

RaiseException函数分析

RaiseException函数流程:

1)先填充异常结构体 \_EXCEPTION\_RECORD

2)然后调用 NT!NtRaiseException

RaiseException函数:

模拟异常的 ExceptionCode 有调用者传进来,这个值每个编译器各不相同,相同的编译器这个值是固定的,与CPU产生异常的值不同。

模拟异常的 ExceptionAddress 保存的并不是真正发生异常的位置,保存的是 RaiseException 函数的地址,而CPU产生异常时保存的是异常发生的真是位置。

 void __stdcall RaiseException(DWORD dwExceptionCode, DWORD dwExceptionFlags, DWORD nNumberOfArguments, const ULONG_PTR *lpArguments)
                 public RaiseException
 RaiseException  proc near               ; CODE XREF: OutputDebugStringA+4F↓p
                                         ; DATA XREF: .text:off_7C802654↑o ...

 ExceptionRecord = _EXCEPTION_RECORD ptr -50h
 dwExceptionCode = dword ptr  8
 dwExceptionFlags= dword ptr  0Ch
 nNumberOfArguments= dword ptr  10h
 lpArguments     = dword ptr  14h
 arg_14          = word ptr  1Ch
 arg_18          = dword ptr  20h

 ; FUNCTION CHUNK AT .text:7C8449F0 SIZE 00000008 BYTES
 ; FUNCTION CHUNK AT .text:7C84B6A4 SIZE 00000037 BYTES
		
                 mov     edi, edi
                 push    ebp
                 mov     ebp, esp
				 //上面初始化堆栈

                 sub     esp, 50h  //分配 50h的临时大小保存异常结构体

				 //为异常结构体赋值
                 mov     eax, [ebp+dwExceptionCode]  //由调用者传进来
                 and     [ebp+ExceptionRecord.ExceptionRecord], 0
                 mov     [ebp+ExceptionRecord.ExceptionCode], eax
                 mov     eax, [ebp+dwExceptionFlags]
                 push    esi
                 mov     esi, [ebp+lpArguments]

                 and     eax, 1
                 test    esi, esi
                 mov     [ebp+ExceptionRecord.ExceptionFlags], eax  //ExceptionFlags = 1 //软件模拟产生的异常
 
                 mov     [ebp+ExceptionRecord.ExceptionAddress], offset RaiseException
                 jz      loc_7C812B60
                 mov     ecx, [ebp+nNumberOfArguments]
                 cmp     ecx, 0Fh
                 ja      loc_7C8449F0

 loc_7C812AD3:                           ; CODE XREF: RaiseException+31F5A↓j
                 test    ecx, ecx
                 mov     [ebp+ExceptionRecord.NumberParameters], ecx
                 jz      short loc_7C812AE1
                 push    edi
                 lea     edi, [ebp+ExceptionRecord.ExceptionInformation]
                 rep movsd
                 pop     edi

 loc_7C812AE1:                           ; CODE XREF: RaiseException+3F↑j
                                         ; RaiseException+CB↓j
                 lea     eax, [ebp+ExceptionRecord]
                 push    eax             ; ExceptionRecord
                 call    ds:RtlRaiseException                   //调用 KiRaiseException 函数
                 pop     esi
                 leave
                 retn    10h
 ;---------------------------------------------------------------------------

 loc_7C812AF0:                           ; CODE XREF: sub_7C80BDA9+53↑j
                 test    edi, edi
                 jle     loc_7C80BE2E
                 mov     edx, [ebp+ExceptionRecord.ExceptionInformation+38h]
                 mov     [ebp+dwExceptionFlags], edx

 loc_7C812AFE:                           ; CODE XREF: RaiseException+98↓j
                 movzx   edx, word ptr [esi]
                 mov     edi, [ebp+ExceptionRecord.ExceptionInformation+34h]
                 mov     dl, [edx+edi]
                 mov     [ecx], dl
                 mov     edi, [eax+0Ch]
                 movzx   edx, dl
                 mov     dx, [edi+edx*2]
                 cmp     dx, [esi]
                 jnz     loc_7C84B6A4

 loc_7C812B1C:                           ; CODE XREF: RaiseException+38C13↓j
                 mov     edx, [eax+8]
                 mov     bx, [edx+4]
                 cmp     [ecx], bl
                 jz      loc_7C84B6B1

 loc_7C812B2B:                           ; CODE XREF: RaiseException+38C1F↓j
                                         ; RaiseException+38C32↓j ...
                 inc     esi
                 inc     esi
                 inc     ecx
                 dec     [ebp+dwExceptionFlags]
                 jnz     short loc_7C812AFE
                 jmp     loc_7C80BE2E
 ; ---------------------------------------------------------------------------

 loc_7C812B38:                           ; CODE XREF: LCMapStringW+11E↑j
                 mov     ecx, [ebp+nNumberOfArguments]
                 call    sub_7C80A364
                 mov     edx, [ebp+dwExceptionFlags]
                 mov     ebx, eax
                 inc     ebx
                 jmp     loc_7C80CE5C
 ; ---------------------------------------------------------------------------

 loc_7C812B4B:                           ; CODE XREF: LCMapStringW+243↑j
                 mov     ebx, ecx
                 mov     [ebp+dwExceptionCode], ebx
                 jmp     loc_7C80CD5B
 ; ---------------------------------------------------------------------------

 loc_7C812B55:                           ; CODE XREF: LCMapStringW+237↑j
                 mov     esi, dword_7C88579C
                 jmp     loc_7C80CD61
 ; ---------------------------------------------------------------------------

 loc_7C812B60:                           ; CODE XREF: RaiseException+28↑j
                 and     [ebp+ExceptionRecord.NumberParameters], 0
                 jmp     loc_7C812AE1
 RaiseException  endp

KiRaiseException函数分析

1)EXCEPTION_RECORD.ExceptionCode 最高位清零,用于区分CPU异常

2)调用 KiDispatchException 开始分发异常

异常分发 (KiDispatchException)

内核异常和用户异常都通过函数 KiDispatchException 进行分发。

内核异常分发流程:

内核从的异常因为是在0环所以不需要切换堆栈,因为异常处理函数也在0环。

1)调用 KeContextFromKframes 将 Trap_frame 备份到 context 为返回3环做准备

2)判断先前模式 : 0 是内核调用   1是用户调用

3)判断是否是第一次机会处理异常

4)是否存在内核调试器,如windbg

5)如果没有内核调试器或者内核调试器不处理

6)调用 RtlDispatchException 函数,3环处理异常,RtlDispatchException函数主要是寻找处理异常的函数,并调用
   通过,FS:[0]查找,FS:[0]即KPCR的第一个成员,指向一个处理异常的函数的单项链表。

7)如果返回 FALSE 

8)再次判断是否存在内核调试器,有调用,没有直接蓝屏 (注:第二次机会,只会判断是否存在内核调试器,并不会再次交给3环处理。)

用户异常分发流程:

1)KeContextFromKframes 将 Trap_frame 备份到 context 为返回3环做准备。

2)判断先前模式 0是内核调用  1是用户调用

3)是否是第一次机会

4)是否有内核调试器

5)发送给三环调试器

6)如果3环调试器没有处理这个异常,则修改 EIP为 KiUserExceptionDispatcher(三环处理异常函数) 函数地址

7)KiDispatchException函数执行结束:CPU异常与模拟异常返回地点不同

	CPU异常:CPU检测到异常 → 查IDT执行处理函数 → CommonDispatchException

			→ KiDispatchException 通过 IRETD 返回3环(中断返回)


	模拟异常:CxxThrowException → RaiseException → RrlRaiseException

			 → NT!NtRaiseException → NT!KiRaiseException

			 → KiDispatchException 通过系统调用返回3环


8)无论通过那种方式,当线程再次回到3环时,将执行KiUserExceptionDispatchar函数。

当异常发生在3环,就意味着要切换堆栈,回到3环执行处理函数。

切换堆栈的处理方式与用户APC的执行过程几乎是一样的,唯一的区别就是执行用户APC时,返回3环后执行的函数是 KiUserApcDispatcher,而异常处理时返回3环后执行的函数是KiUserExceptionDispatcher。

KiDispatchException函数分析 (内核异常和用户异常都通过这个函数分发)

内核的异常可以直接再内核中处理,但是用户的异常需要返回三环处理,所以需要切换堆栈。

VOID KiDispatchException (
    IN PEXCEPTION_RECORD ExceptionRecord,
    IN PKEXCEPTION_FRAME ExceptionFrame,
    IN PKTRAP_FRAME TrapFrame,
    IN KPROCESSOR_MODE PreviousMode,
    IN BOOLEAN FirstChance
    )

(注:用户和内核分发是一个整体的函数流程要结合起来看)

KiDispatchException函数中内核分发过程:

 ; int __stdcall KiDispatchException(PEXCEPTION_RECORD ExceptionRecord, int, ULONG_PTR BugCheckParameter3, int, char)
 _KiDispatchException@20 proc near       ; CODE XREF: CommonDispatchException+48↑p
                                         ; KiRaiseException(x,x,x,x,x)+158↓p ...

 var_3A0         = dword ptr -3A0h
 var_394         = dword ptr -394h
 var_350         = _EXCEPTION_RECORD ptr -350h
 var_300         = dword ptr -300h
 var_2FC         = dword ptr -2FCh
 var_2F8         = dword ptr -2F8h
 var_2F4         = dword ptr -2F4h
 var_2F0         = dword ptr -2F0h
 var_2EC         = dword ptr -2ECh
 Context         = CONTEXT ptr -2E8h
 var_1C          = dword ptr -1Ch
 ms_exc          = CPPEH_RECORD ptr -18h
 ExceptionRecord = dword ptr  8
 arg_4           = dword ptr  0Ch
 BugCheckParameter3= dword ptr  10h
 arg_C           = dword ptr  14h
 arg_10          = byte ptr  18h

 ; FUNCTION CHUNK AT .text:0042C0C2 SIZE 0000019F BYTES
 ; FUNCTION CHUNK AT .text:0042C274 SIZE 00000026 BYTES
 ; FUNCTION CHUNK AT .text:004309A1 SIZE 0000001F BYTES
 ; FUNCTION CHUNK AT .text:00446746 SIZE 00000082 BYTES
 ; FUNCTION CHUNK AT .text:004467CD SIZE 00000036 BYTES
 ; FUNCTION CHUNK AT .text:0044680D SIZE 00000012 BYTES
 ; FUNCTION CHUNK AT .text:00446824 SIZE 00000089 BYTES

 ; __unwind { // __SEH_prolog
                 push    390h
                 push    offset stru_42BD90
                 call    __SEH_prolog
                 mov     eax, ds:___security_cookie

                 mov     [ebp+var_1C], eax  //将 eax 保存到临时变量
                 mov     esi, [ebp+ExceptionRecord]  //获取 保存异常结构体
                 mov     [ebp+var_2EC], esi          //将异常结构体保存一份到临时变量

				 //此时 esi = _EXCEPTION_RECORD

                 mov     ecx, [ebp+arg_4]            //获取传入的第2个参数 ExceptionFrame
                 mov     [ebp+var_2F0], ecx          //将 ecx = ExceptionFrame 保存到临时变量

                 mov     ebx, [ebp+BugCheckParameter3]   //获取参数 TrapFrame
                 mov     [ebp+var_2F8], ebx              //将 TrapFrame 保存到临时变量 

                 db      3Eh 
                 mov     eax, ds:0FFDFF020h      //FFDFF000指向当前CPU的KPCR, KPCR + 20 = KPCR.Prcb 获取KPCRB的地址
				 
				 //此时eax = KPCRB

                 inc     dword ptr [eax+504h]    //增加 KPCRB.KeExceptionDispatchCount 的计数
                 mov     [ebp+Context.ContextFlags], 10017h  //Context.ContextFlags 上下文结构体 = 10017h
                 cmp     byte ptr [ebp+arg_C], 1   //比较 第二个参数 ExceptionFrame 是否为1 
                 jz      loc_42C274                //等于 1 跳转

                 cmp     ds:_KdDebuggerEnabled, 0  //比较 _KdDebuggerEnabled 是否为0
                 jnz     loc_42C274                //不为 0 跳转


 loc_42BCFE:                             ; CODE XREF: KiDispatchException(x,x,x,x,x)+5E6↓j
                                         ; KiDispatchException(x,x,x,x,x)+5F6↓j
				 //将数据保存到上下文结构体中
				 //要返回三环就要修改EIP的值,并且修改堆栈,用Context保存内核堆栈的数据
                 lea     eax, [ebp+Context]   //上下文结构体首地址
                 push    eax                  //接受一个用于保存当前数据的上下文结构体首地址
                 push    ecx
                 push    ebx                  //参数 _Trap_Frame
                 call    _KeContextFromKframes@12 ; KeContextFromKframes(x,x,x)  //将_Trap_Frame备份到Context中,准备返回三环

                 mov     eax, [esi]
                 cmp     eax, 80000003h
                 jnz     loc_42C21F
                 dec     [ebp+Context._Eip]

 loc_42BD1F:                             ; CODE XREF: KiDispatchException(x,x,x,x,x)+585↓j
                                         ; KiDispatchException(x,x,x,x,x)+595↓j ...
                 xor     edi, edi

 loc_42BD21:                             ; CODE XREF: KiDispatchException(x,x,x,x,x)+1AAEE↓j

				 //判断用户异常还是内核异常 
                 cmp     byte ptr [ebp+arg_C], 0   //ebp+14h = 第4个参数的值 = 先前模式  0 内核, 1用户   
				 jnz     loc_42C0C2                //不为 0 跳转,前往用户异常 
				
				 //内核异常
                 cmp     [ebp+arg_10], 1           //第5和参数 FirstChance, 判断是否是第一次调用
                 jnz     loc_44679A                //不为 0 则跳转

                 mov     eax, ds:_KiDebugRoutine   //获取调试器的值
                 cmp     eax, edi                  //判断是否存在调试器如windbg,_KiDebugRoutine == 0则不存在
                 jz      loc_4309A1                //_KiDebugRoutine == 0 跳转
				 
				 //如果有内核调试器,则调用内核调试器
                 push    edi                       // 0  , 第一次传 0,否则传1 
                 push    edi                       // 0 , 内核传0, 用户传1
                 lea     ecx, [ebp+Context]
                 push    ecx                       //Context 结构体
                 push    esi                       //esi = _EXCEPTION_RECORD 记录异常结构体
                 push    [ebp+var_2F0]             //ExceptionFrame参数
                 push    ebx                       //参数 _Trap_Frame
                 call    eax ; _KiDebugRoutine     //调用函数 -- _KiDebugRoutine 调用内核调试器 

				 //如果调用放回成功,说明内核调试器已经处理了,就将CONTEXT再转成Trap_Frame直接返回

				 //如果调试器没有处理,那就接着让3环处理

                 test    al, al                    //判断是否调用成功
                 jz      loc_4309A1                //如果内核调试器没有处理,则交给3环处理

 loc_42BD5D:                             ; CODE XREF: KiDispatchException(x,x,x,x,x)+465↓j
                                         ; KiDispatchException(x,x,x,x,x)+5AA↓j ...

				 //将CONTEXT再转成Trap_Frame直接返回
                 push    [ebp+arg_C]                   //ebp+14h = 第4个参数的值 = 先前模式  0 内核, 1用户   
                 push    [ebp+Context.ContextFlags]     //Context.ContextFlags 上下文结构体 = 10017h
                 lea     eax, [ebp+Context]          
                 push    eax                           //上下文结构体首地址
                 push    [ebp+var_2F0]                 // ExceptionFrame 
                 push    ebx                           //参数 _Trap_Frame
                 call    _KeContextToKframes@20 ; KeContextToKframes(x,x,x,x,x)

 loc_42BD79:                             ; CODE XREF: KiDispatchException(x,x,x,x,x)+476↓j
                                         ; KiDispatchException(x,x,x,x,x)+57B↓j ...
                 mov     ecx, [ebp+var_1C]
                 call    sub_40D361
                 call    __SEH_epilog
                 retn    14h
 ; } // starts at 42BC9F
 _KiDispatchException@20 endp


 loc_42C274:                             ; CODE XREF: KiDispatchException(x,x,x,x,x)+4C↑j
                                         ; KiDispatchException(x,x,x,x,x)+59↑j
 ; __unwind { // __SEH_prolog
                 mov     [ebp+Context.ContextFlags], 1001Fh  //Context.ContextFlags 上下文结构体 = 1001Fh
                 cmp     ds:_KeI386XMMIPresent, 0            
                 jz      loc_42BCFE                          //_KeI386XMMIPresent == 0 跳转
                 mov     [ebp+Context.ContextFlags], 1003Fh  //Context.ContextFlags 上下文结构体 = 1003Fh
                 jmp     loc_42BCFE
 ; } // starts at 42C274

		--------------------------------------------------------------------------------------------------------

 //内核异常,并且是第二次调用,第二次并不会交给3环处理,没有调试器直接蓝屏
 loc_44679A:                             ; CODE XREF: KiDispatchException(x,x,x,x,x)+90↑j
                 mov     eax, ds:_KiDebugRoutine   //获取调试器的值 _KiDebugRoutine
                 cmp     eax, edi                  //判断是否存在调试器如windbg,_KiDebugRoutine == 0则不存在
                 jz      loc_44689C                //_KiDebugRoutine == 0 跳转
				 
				 //如果有内核调试器,则调用内核调试器
                 push    1                    // 1  , 第一次传 0,否则传1 
                 push    edi                  // 0 , 内核传0, 用户传1
                 lea     ecx, [ebp+Context]
                 push    ecx                  //Context 结构体
                 push    esi                  //esi = _EXCEPTION_RECORD 记录异常结构体
                 push    [ebp+var_2F0]        //ExceptionFrame参数
                 push    ebx                  //参数 _Trap_Frame
                 call    eax ; _KiDebugRoutine //调用函数 -- _KiDebugRoutine

				 //如果调用放回成功,说明内核调试器已经处理了,就将CONTEXT再转成Trap_Frame直接返回

				 //如果调试器没有处理,那就接着让3环处理

                 test    al, al
                 jz      loc_44689C          //如果内核调试器处理失败,蓝屏
                 jmp     loc_42BD5D          //成功,就将CONTEXT再转成Trap_Frame直接返回
 ; } // starts at 446746

	-------------------------------------------------------------------------
 
 //内核异常不存在调试器,并且是第一次调用
 loc_4309A1:                             ; CODE XREF: KiDispatchException(x,x,x,x,x)+9D↑j
                                         ; KiDispatchException(x,x,x,x,x)+B8↑j
 ; __unwind { // __SEH_prolog
                 lea     eax, [ebp+Context]
                 push    eax                //上下文参数 Context  
                 push    esi                // ExceptionRecord 记录异常结构体
                 call    _RtlDispatchException@8 ; RtlDispatchException(x,x)  //调用函数处理异常
                 jmp     loc_446792   //3环没处理,进行第二次异常调用判断

	-------------------------------------------------------------------------------------------
 loc_44689C:                             ; CODE XREF: KiDispatchException(x,x,x,x,x)+1AB02↑j
                                         ; KiDispatchException(x,x,x,x,x)+1AB1E↑j ...

                 push    edi             ; BugCheckParameter4        // 0
                 push    ebx             ; BugCheckParameter3        //参数 _Trap_Frame
                 push    dword ptr [esi+0Ch] ; BugCheckParameter2    //ExceptionRecord.ExceptionAddress 异常发生地址
                 push    dword ptr [esi] ; BugCheckParameter1        // ExceptionRecord 记录异常结构体
                 push    8Eh             ; BugCheckCode              // 8Eh
                 call    _KeBugCheckEx@20 ; KeBugCheckEx(x,x,x,x,x)  //第二次 也失败 蓝屏
 ; } // starts at 446824
 ; END OF FUNCTION CHUNK FOR KiDispatchException(x,x,x,x,x)

KiDispatchException函数中用户分发过程:

用户流程是通过判断先前模式跳转过来的。

 loc_42C0C2:                             ; CODE XREF: KiDispatchException(x,x,x,x,x)+86↑j
 ; __unwind { // __SEH_prolog

                 cmp     [ebp+arg_10], 1   //第5和参数 FirstChance, 判断是否是第一次调用
                 jnz     loc_446870        //不为 0 则跳转
				 
				 //用户第一次调用
                 cmp     ds:_KiDebugRoutine, edi  //判断内核调试器的值是否为空,即是否存在内核调试器
                 jz      short loc_42C10A         //_KiDebugRoutine == 0 跳转,即内核调试器不存在

                 mov     eax, large fs:124h       //fs:124h = KPCR.CurrentThread 当前线程结构体
                 mov     eax, [eax+44h]           //_KThread._KAPC_STATE.Process 
												  //获取提供CR0的进程结构体(有可能会被挂靠) _KPROCESS
                 cmp     [eax+0BCh], edi          //判断 _KPROCESS.DebugPort 是否为 0  
                 jnz     loc_4467CD               //不为 0 则跳转

 loc_42C0E9:                             ; CODE XREF: KiDispatchException(x,x,x,x,x)+4D16↓j	
				 //第一次处理用户异常,并且调试器存在
                 push    edi                  // 0  , 第一次传 0,否则传1 
                 push    [ebp+arg_C]         //ebp+14h = 第4个参数的值 = 先前模式  0 内核, 1用户 
                 lea     eax, [ebp+Context]  
                 push    eax                 //Context 结构体
                 push    esi                 //esi = _EXCEPTION_RECORD 记录异常结构体
                 push    [ebp+var_2F0]       //ExceptionFrame参数
                 push    ebx                 //参数 _Trap_Frame
                 call    ds:_KiDebugRoutine   //调用函数 -- _KiDebugRoutine
                 test    al, al
                 jnz     loc_42BD5D           // al == 1 跳转,即调试器处理成功,还原Trap_Frame返回成功 

 loc_42C10A:                             ; CODE XREF: KiDispatchException(x,x,x,x,x)+433↑j
                                         ; KiDispatchException(x,x,x,x,x)+4D1C↓j

				 //尝试调用三环调试器
                 push    edi         // 0
                 push    1
                 push    esi         //esi = _EXCEPTION_RECORD 记录异常结构体
                 call    _DbgkForwardException@12 ; DbgkForwardException(x,x,x) //发送给三环调试器
												  //参数1:ExceptionRecord
												  //参数2:调试端口 TRUE,异常端口 FALSE
												  //参数3:是否是第二次机会 1 FALSE, 2 TRUE
                 test    al, al 
                 jnz     loc_42BD79   // al == 1 跳转即三环调试器处理了,并返回成功,DbgkForwardException函数中已经还原Trap_Frame

				 //三环调试器处理失败
                 mov     [ebp+var_3A0], edi  //将 0 保存到临时变量

 loc_42C121:                             ; CODE XREF: KiDispatchException(x,x,x,x,x)+1ABBA↓j
				 
				 //切换到三环,即修改 _Trap_Frame 的值
                 mov     [ebp+ms_exc.registration.TryLevel], edi
                 cmp     dword ptr [ebx+78h], 23h ; 
                 jnz     loc_4467E1

                 test    byte ptr [ebx+72h], 2
                 jnz     loc_4467E1

                 mov     eax, 2CCh                 //eax = 2CCh
                 mov     [ebp+var_2FC], eax        //将 2CCh 保存到临时变量
                 mov     edi, [ebp+Context._Esp]   // edi = Context._Esp 保存内核栈的栈顶

                 and     edi, 0FFFFFFFCh           //对Esp进行4字节对齐
                 sub     edi, eax                  // 提升3环栈顶 大小为 2CCh 
                 mov     [ebp+var_2F4], edi       //将提升后的栈顶保存到临时变量

                 push    4               ; Alignment
                 push    eax             ; Length
                 push    edi             ; Address
                 call    _ProbeForWrite@12 ; ProbeForWrite(x,x,x)  //检查内存是否可写

                 mov     ecx, 0B3h          //Context的大小 为 0B3h
                 lea     esi, [ebp+Context]
                 rep movsd      //将 esi的值拷贝到edi的位置,大小为 0B3h, 即拷贝一份Context出来准备保存到三环堆栈 
								//edi的位置为内核栈的栈顶

                 mov     eax, [ebp+var_2EC]  //获取保存到临时变量中的异常结构体 _EXCEPTION_RECORD
                 mov     esi, [eax+10h]      // 获取附加参数个数 _EXCEPTION_RECORD.NumberParameters

                 lea     esi, ds:17h[esi*4]  //获取第一个参数的地址
                 and     esi, 0FFFFFFFCh     //将参数的地址进行4字节对齐
                 mov     [ebp+var_2FC], esi  //将参数地址保存到临时变量

                 mov     edi, [ebp+var_2F4]  //获取提升后堆栈栈顶
                 sub     edi, esi            //再提升栈顶 用来保存参数

                 mov     [ebp+var_300], edi  //将新栈顶保存到临时变量
                 push    4               ; Alignment
                 lea     eax, [esi+8]
                 push    eax             ; Length
                 lea     eax, [edi-8]
                 push    eax             ; Address
                 call    _ProbeForWrite@12 ; ProbeForWrite(x,x,x)  //检查内存是否可写

                 mov     ecx, esi
                 mov     esi, [ebp+var_2EC] //获取保存到临时变量中的异常结构体 _EXCEPTION_RECORD

                 mov     eax, ecx
                 shr     ecx, 2
                 rep movsd

                 mov     ecx, eax
                 and     ecx, 3
                 rep movsb

                 mov     ecx, [ebp+var_2F4]  //获取提升后堆栈栈顶,即新的Context的栈顶
                 mov     eax, [ebp+var_300]  //获取带参数的栈顶
                 mov     [eax-4], ecx
                 lea     edx, [eax-8]
                 mov     [edx], eax
                 push    20h ; ' '
                 push    ebx                //参数 _Trap_Frame
                 call    _KiSegSsToTrapFrame@8 ; KiSegSsToTrapFrame(x,x)  //修改 TrapFrame 的SS

                 push    edx             ; BugCheckParameter1  // ExceptionRecord 记录异常结构体
                 push    ebx             ; int                 //参数 _Trap_Frame
                 call    _KiEspToTrapFrame@8 ; KiEspToTrapFrame(x,x)      //修改 TrapFrame 的ESP为 三环的栈顶

                 mov     eax, [ebp+arg_C]
                 mov     cl, al
                 neg     cl
                 sbb     ecx, ecx
                 and     ecx, 3
                 add     ecx, 18h
                 mov     [ebx+6Ch], ecx
                 mov     cl, al
                 neg     cl
                 sbb     ecx, ecx
                 and     ecx, 3
                 add     ecx, 20h ; ' '
                 mov     [ebx+38h], ecx
                 mov     [ebx+34h], ecx
                 neg     al
                 sbb     eax, eax
                 and     eax, 3
                 add     eax, 38h ; '8'
                 mov     [ebx+50h], eax
                 and     dword ptr [ebx+30h], 0

                 mov     eax, ds:_KeUserExceptionDispatcher  //_KeUserExceptionDispatcher 保存处理3环异常函数的地址(KiUserExceptionDispatcher)
     (关键修改)   mov     [ebx+68h], eax                      //修改 TrapFrame.Eip, 即设置回到3环后执行的位置 (并没有在这里返回三环)
                 or      [ebp+ms_exc.registration.TryLevel], 0FFFFFFFFh
                 jmp     loc_42BD79                          //结束执行,但此时并没有返回三环
 ; ---------------------------------------------------------------------------

 loc_42C21F:                             ; CODE XREF: KiDispatchException(x,x,x,x,x)+74↑j
                 cmp     eax, 10000004h
                 jnz     loc_42BD1F
                 mov     dword ptr [esi], 0C0000005h
                 cmp     byte ptr [ebp+arg_C], 1
                 jnz     loc_42BD1F
                 lea     eax, [ebp+Context]
                 push    eax
                 push    esi
                 call    _KiCheckForAtlThunk@8 ; KiCheckForAtlThunk(x,x)
                 test    al, al
                 jnz     loc_42BD5D
                 cmp     byte ptr ds:0FFDF0280h, 1
                 jnz     loc_42BD1F
                 jmp     loc_446746
 ; } // starts at 42C0C2
 ; END OF FUNCTION CHUNK FOR KiDispatchException(x,x,x,x,x)

#异常处理

用户异常:当用户异常产生后,内核函数KiDispatchException并不是像处理内核异常那样在0环直接进行处理,而是修正3环EIP为KiUserExceptionDispatcher函数后就结束了。这样,当线程再次回到3环时,将会从KiUserExceptionDispatcher函数开始执行。

VEH(向量化异常处理)– 仅用户模式下可以使用

VEH链表(全局异常链表),是一个全局的可以调用函数插入。

VEH异常的处理流程:

1)CPU捕获异常信息

2)通过 KiDispatchException 进行分发 (设置 EIP = KiUserExceptionDispatcher)

3)KiUserExceptionDispatcher 调用 KiDispatchException

4)KiDispatchException 查找VEH处理函数链表并调用相关处理函数

5)代码返回到 KiUserExceptionDispatcher 函数

6)调用 ZwContinue 再次进入0环(ZwContinue调用NtContinue,返回0环的主要作用就是恢复_TRAP_FRAME然后通过_KiServicrExit返回到3环)。

7)线程再次返回3环后,从修正后的位置开始执行。

SEH(结构化异常) – 用户模式、内核模式下均可使用

SEH保存再线程堆栈中的异常链表,每个线程都有自己的SEH链表,其中Handle指向的异常函数是具有一定格式的,要我们自己定义。SEH异常处理是windwos平台所特有的。

异常处理函数结构体(SEH使用,保存在堆栈中):

typedef struct _EXCEPTION_REGISTRATION_RECORD
{
	struct _EXCEPTION_REGISTRATION_RECORD *Next;  //指向下一个异常结构体
	
	PEXCEPTION_ROUTINE Handler;                   //指向异常处理函数

}EXCEPTION_REGISTRATION_RECORD

这个结构体保存在线程的堆栈里。

内核层中,FS:[0] 指向 KPCR, 而KPCR的第一个值就是这个异常处理函数结构体。

单向链表结构:

FS:[0]  →    Next       →    ↓
             Handler          

             .....           ↓

	↓	←	 Next   ←    ←   ←
			 Handle (异常处理函数)

	↓		 ....

			 -1 (0xFFFFFFFF)               (最后一个)
	→ 	→ 	 Handle  (异常处理函数)

用户层中,FS:[0] 指向 _TEB,而TEB的第一个成员也这个一个异常处理结构体:

TEB.NtTib.ExceptionList

SEH异常的处理流程:

1)FS:[0] 指向 SEH链表的第一个成员

2)SEH的异常处理函数的结构体(_EXCEPTION_REGISTRATION_RECORD)必须在当前线程的堆栈中

3)只有当VEH中的异常处理函数不存在或者不处理才会到SEH链表中查找

手动挂载SEH异常处理函数:

SEH的异常函数是需要我们手动去挂载的。

_asm
{
	//将FS:[0]指向自己设置的异常结构体,保存之前第一个异常结构体指针
	mov eax, FS:[0]
	mov temp, eax
	lea ecx, myException
	mov FS:[0], ecx
}

myException.prev = (MyException*) temp; //设置自己的异常结构体的指向下一个成员为之前第一个异常结构体指针
myException.handle = (DWORD)&MyException_handler; //自己定义的异常处理函数,具有一定格式

//自定义异常处理函数,函数定义格式固定
EXCEPTION_DISPOSITION _cdecl MyException_handler(
	struct _EXCEPTION_RECORD *ExceptionRecord,
	void *EstablisherFrae,
	struct_CONTEXT *ContextRecord,
	void *DispatcherContext
	)
{
	if (ExceptionRecord->ExceptionCode == 0xC0000094) //异常过滤(除零异常)
	{
		//处理异常
		ContextRecord->Eip = ContextRecord->Eip + 2; //比如,跳过除零代码

		return ExceptionContinueExecution; //返回出错位置重新执行
	}
	
	return ExceptionContinueSearch;  //寻找下一个异常处理函数
};

在异常处理结束后,会返回0环,在0环将 ContextRecord 赋值给 Trap_frame,当再次回到3环时通过ContextRecord.Eip的位置开始执行。

KiUserExceptionDispatcher函数分析

KiUserExceptionDispatcher函数流程:

1)调用 RtlDispatchException 查找并执行异常处理函数

2)如果 RtlDispatchException 返回为真,调用 ZwContinue 再次进入0环,当线程再次返回3环时,会从修正后的位置开始执行。

3)如果 RtlDispatchException 返回为假,调用 ZwRaiseException 进行第二轮异常分发。

KiUserExceptionDispatcher函数分析:

 ; __stdcall KiUserExceptionDispatcher(x, x)
                 public _KiUserExceptionDispatcher@8
 _KiUserExceptionDispatcher@8 proc near  ; DATA XREF: .text:off_7C923428↑o

 var_C           = dword ptr -0Ch
 var_8           = dword ptr -8
 var_4           = dword ptr -4
 arg_0           = dword ptr  4

                 mov     ecx, [esp+arg_0]
                 mov     ebx, [esp+0]
                 push    ecx
                 push    ebx
                 call    _RtlDispatchException@8 ; RtlDispatchException(x,x) //调用RtlDispatchException查找异常处理函数
                 or      al, al
                 jz      short loc_7C92E47A  //如果没有找到异常处理函数,跳转
				 
				 //如果函数 RtlDispatchException 调用成功返回 0环
                 pop     ebx
                 pop     ecx
                 push    0
                 push    ecx
                 call    _ZwContinue@8   ; ZwContinue(x,x) //调用当前函数返回 0环
                 jmp     short loc_7C92E485
 ; ---------------------------------------------------------------------------

 
 //没有找到异常处理函数
 loc_7C92E47A:                           ; CODE XREF: KiUserExceptionDispatcher(x,x)+10↑j
                 pop     ebx
                 pop     ecx
                 push    0
                 push    ecx
                 push    ebx
                 call    _ZwRaiseException@12 ; ZwRaiseException(x,x,x)  //进行第二次异常分发

 loc_7C92E485:                           ; CODE XREF: KiUserExceptionDispatcher(x,x)+1C↑j
                 add     esp, 0FFFFFFECh
                 mov     [esp+0Ch+var_C], eax
                 mov     [esp+0Ch+var_8], 1
                 mov     [esp+0Ch+var_4], ebx
                 mov     [esp+0Ch+arg_0], 0
                 push    esp             ; ExceptionRecord
                 call    _RtlRaiseException@4 ; RtlRaiseException(x) 
 _KiUserExceptionDispatcher@8 endp ; sp-analysis failed

 ; ---------------------------------------------------------------------------
                 retn    8
 ; Exported entry  44. KiRaiseUserExceptionDispatcher

之所以要通过 ZwContinue 函数返回0环是因为,当发生异常时的数据保存在0环,要返回0环重新修正EIP的位置,再次返回3环。

RtlDispatchException分析 (用户层异常分发函数分析)

RtlDispatchException函数作用:

遍历异常链表,调用异常处理函数,如果异常被正确处理,该函数返回1。

如果当前异常处理函数不能处理该异常,那么调用下一个,以此类推。

如果到最后也没有人处理这个异常,则返回0。

RtlDispatchException流程:

1)查找VEH链表(全局链表),如果有则调用

2)查找SEH链表(局部链表,在堆栈中),如果有则调用

与内核调用不同。

RtlDispatchException分析:

 ; __stdcall RtlDispatchException(x, x)
 _RtlDispatchException@8 proc near       ; CODE XREF: KiUserExceptionDispatcher(x,x)+9↑p

 ExceptionRecord = EXCEPTION_RECORD ptr -64h
 var_14          = dword ptr -14h
 var_10          = dword ptr -10h
 var_C           = dword ptr -0Ch
 var_8           = dword ptr -8
 var_1           = byte ptr -1
 arg_0           = dword ptr  8
 arg_4           = dword ptr  0Ch

 ; FUNCTION CHUNK AT .text:7C94A2F5 SIZE 0000002A BYTES
 ; FUNCTION CHUNK AT .text:7C96E2DA SIZE 00000099 BYTES

                 mov     edi, edi
                 push    ebp
                 mov     ebp, esp
                 sub     esp, 64h
				 //上面初始化函数堆栈					 

                 push    esi
                 push    [ebp+arg_4]
                 mov     esi, [ebp+arg_0]
                 push    esi
                 mov     [ebp+var_1], 0
                 call    _RtlCallVectoredExceptionHandlers@8 ; RtlCallVectoredExceptionHandlers(x,x) //先查VEH异常链表

                 test    al, al
                 jnz     loc_7C96E2DA
                 push    ebx
                 lea     eax, [ebp+var_C]
                 push    eax
                 lea     eax, [ebp+var_8]
                 push    eax
                 call    _RtlpGetStackLimits@8 ; RtlpGetStackLimits(x,x)  //获取线程的开始位置和范围
                 call    _RtlpGetRegistrationHead@0 ; RtlpGetRegistrationHead() //获取SEH异常链表头

                 and     [ebp+arg_0], 0
                 mov     ebx, eax
                 cmp     ebx, 0FFFFFFFFh
                 jz      loc_7C94AA22
                 push    edi

 loc_7C94A994:                           ; CODE XREF: RtlDispatchException(x,x)-645↑j
                 cmp     ebx, [ebp+var_8]
                 jb      loc_7C94A316
                 lea     eax, [ebx+8]
                 cmp     eax, [ebp+var_C]
                 ja      loc_7C94A316
                 test    bl, 3
                 jnz     loc_7C94A316
                 mov     eax, [ebx+4]
                 cmp     eax, [ebp+var_8]
                 jb      short loc_7C94A9C3
                 cmp     eax, [ebp+var_C]
                 jb      loc_7C94A316

 loc_7C94A9C3:                           ; CODE XREF: RtlDispatchException(x,x)+68↑j
                 push    eax
                 call    _RtlIsValidHandler@4 ; RtlIsValidHandler(x)
                 test    al, al
                 jz      loc_7C94A316
                 test    byte_7C99B3FA, 80h
                 jnz     loc_7C96E2E3

 loc_7C94A9DE:                           ; CODE XREF: RtlDispatchException(x,x)+239A4↓j
                 push    dword ptr [ebx+4]
                 lea     eax, [ebp+var_14]
                 push    eax
                 push    [ebp+arg_4]
                 push    ebx
                 push    esi
                 call    _RtlpExecuteHandlerForException@20 ; RtlpExecuteHandlerForException(x,x,x,x,x)  
				 //上面这个函数调用SEH中Handler指向的异常处理函数。

                 test    byte_7C99B3FA, 80h
                 mov     edi, eax
                 jnz     loc_7C96E2F9

 loc_7C94A9FE:                           ; CODE XREF: RtlDispatchException(x,x)+239B2↓j
                 cmp     [ebp+arg_0], ebx
                 jz      loc_7C96E307

 loc_7C94AA07:                           ; CODE XREF: RtlDispatchException(x,x)+239BF↓j
                 mov     eax, edi
                 xor     ecx, ecx
                 sub     eax, ecx
                 jnz     loc_7C94A2F5
                 test    byte ptr [esi+4], 1
                 jnz     loc_7C96E351
                 mov     [ebp+var_1], 1

 loc_7C94AA21:                           ; CODE XREF: RtlDispatchException(x,x)-650↑j
                                         ; RtlDispatchException(x,x)-63F↑j ...
                 pop     edi

 loc_7C94AA22:                           ; CODE XREF: RtlDispatchException(x,x)+3D↑j
                 pop     ebx

 loc_7C94AA23:                           ; CODE XREF: RtlDispatchException(x,x)+2398E↓j
                 mov     al, [ebp+var_1]
                 pop     esi
                 leave
                 retn    8
 _RtlDispatchException@8 endp

编译器扩展SEH

下面这种代码执行时,编译器会帮我我们做SEH异常的处理:

_try        //挂入链表
{
	//执行程序
}
_except(过滤表达式)     
{
	//异常处理程序
}

当 try 里面的程序发生异常时,根据过滤表达式提供的值,进行异常处理。

过滤表达式只能返回3个值:

1)EXCEPTION_EXECUTE_HANDLER (1) 执行 excrpt 代码

2)EXCEPTION_CONTINUE_SEARCH (0) 寻找下一个异常处理函数

3)EXCEPTION_CONTINUE_EXECUTION (-1) 返回出错位置重新执行

以函数形式调用 try-except:

以函数形式调用过滤表达式,后再过滤表达式中修改ecx的值,使其修复除零异常,通过函数 GetExceptionInformation() 获取异常信息 LPPEXCEPTION_POINTERS结构体,注意 GetExceptionInformation() 函数只能用在 try-except 中。

GetExceptionInformation can be called only from within the filter expression of a try-except exception handler.

//函数形式过滤表达式
int ExcrptFilter(LPPEXCEPTION_POINTERS pExceptionInfo)
{
	pExceptionInfo.ContextRecord->Ecx = 1;
	return EXCEPTION_CONTINUE_EXECUTION;
}

_try        //挂入链表
{
	_asm
		{
			xor edx, edx
			xor ecx, ecx
			mov eax, 0x10
			idiv ecx       //除零
		}
}
_except(ExceptFilter(GetExceptionInformation()))     
{
	//异常处理程序
}

try-except实现细节

当系统调用 try-except 时:

int CCeshi4Dlg::cehesi()
{
	_try        
	{
		DWORD64 dwDiskResidueCapacity = 0;
		DWORD64 dwDiskTotalCapacity = 0;
	}
	_except(1)     
	{

	}
	

	return 0;
}	

会给我门生成如下的反汇编代码:

int CCeshi4Dlg::cehesi()
{

	//编译器会使用扩展的异常结构体
	00B44080  push        ebp     //保存ebp,_EXCEPTION_REGISTRION.ebp
	00B44081  mov         ebp,esp 
	00B44083  push        0FFFFFFFEh  // -1, _EXCEPTION_REGISTRION.trylevel 
	00B44085  push        offset __CT??_R0?AVCAtlException@ATL@@@84+64h (0B60218h)  //_EXCEPTION_REGISTRION.scopetable 
	00B4408A  push        offset @ILT+1340(__except_handler4) (0B41541h)  //即要插入到 SEH链表中由编译器指定的异常处理函数
																		  //_EXCEPTION_REGISTRION.scopetable.handler

	00B4408F  mov         eax,dword ptr fs:[00000000h]   //获取 fs:[0]
	00B44095  push        eax                                             //_EXCEPTION_REGISTRION.scopetable.prev
																		  //指向原来的第一个链表头
	00B44096  add         esp,0FFFFFF0Ch 
	00B4409C  push        ebx  
	00B4409D  push        esi  
	00B4409E  push        edi  
	00B4409F  push        ecx  
	00B440A0  lea         edi,[ebp-104h] 
	00B440A6  mov         ecx,3Bh 
	00B440AB  mov         eax,0CCCCCCCCh 
	00B440B0  rep stos    dword ptr es:[edi] 
	00B440B2  pop         ecx  
	00B440B3  mov         eax,dword ptr [___security_cookie (0B61224h)] 
	00B440B8  xor         dword ptr [ebp-8],eax 
	00B440BB  xor         eax,ebp 
	00B440BD  push        eax  
	00B440BE  lea         eax,[ebp-10h]                                   //ebp - 10H 为函数堆栈开始时构建的异常结构体首地址
	00B440C1  mov         dword ptr fs:[00000000h],eax                    //修改FS:[0]指向新的异常结构体
	00B440C7  mov         dword ptr [ebp-18h],esp 
	00B440CA  mov         dword ptr [ebp-20h],ecx 
		_try        
	00B440CD  mov         dword ptr [ebp-4],0       //修改 _EXCEPTION_REGISTRION.trylevel = 0,即执行在第一个try中
		{											
			DWORD64 dwDiskResidueCapacity = 0;
	00B440D4  mov         dword ptr [dwDiskResidueCapacity],0 
	00B440DB  mov         dword ptr [ebp-2Ch],0 
			DWORD64 dwDiskTotalCapacity = 0;
	00B440E2  mov         dword ptr [dwDiskTotalCapacity],0 
	00B440E9  mov         dword ptr [ebp-3Ch],0 
		}
	00B440F0  mov         dword ptr [ebp-4],0FFFFFFFEh  //当try执行完后,将_EXCEPTION_REGISTRION.trylevel = -1
	00B440F7  jmp         $LN6+0Ah (0B44109h) 
		_except(1)     
	00B440F9  mov         eax,1 
	$LN7:
	00B440FE  ret              
	$LN6:
	00B440FF  mov         esp,dword ptr [ebp-18h] 
		{
	
		}
	00B44102  mov         dword ptr [ebp-4],0FFFFFFFEh  //当try执行完后,将_EXCEPTION_REGISTRION.trylevel = -1
														//即没有正在执行的try
		
	
		return 0;
	00B44109  xor         eax,eax 
}	

在当前汇编代码中显示编译器为我们指定了一个异常处理函数:

00B4408A  push        offset @ILT+1340(__except_handler4) (0B41541h)

即:__except_handler4 函数。(不管我们定义了什么样子的异常,操作系统给我们挂入的函数都是固定的)

try-except 嵌套重复与拓展

嵌套重复:

每个使用 try-except 的函数(一个函数),不管其内部嵌套或反复使用多少 try-except,都只注册一遍,即只将一个 _EXCEPTION_REGISTRATION_RECORD 挂入当前线程的异常链表中。对于递归函数,每一次调用都会创建一个 _EXCEPTION_REGISTRATION_RECORD,并挂入进程的异常链表中。即一个函数直挂一个。

拓展:

//原结构体
typedef struct _EXCEPTION_REGISTRATION_RECORD
{

	struct _EXCEPTION_REGISTRATION_RECORD *Next;
	PEXEPTION_ROUTINE Handler;

} EXCEPTION_REGISTRATION_RECORD

编译器为了支持嵌套重复这一特性,对上面这个结构体进行了拓展,但在拓展的时候需要保证前两个成员不做修改:

//扩展后
struct _EXCEPTION_REGISTRION
{

	struct _EXCRPTION_REGISTRION *prev;
	void (*handler)(PEXCEPTION_RECORD, PEXCEPTION_REGISTRION, PCONTEXT, PEXCEPTION_REGISTRION);

	struct scopetable_entry *scopetable;  //保存多个 except 的数组
	int trylevel;   //当前代码正在执行哪一个 try
	int _ebp;

}

扩展后堆栈:

FS:[0]  →    Next       →    ↓
             Handler          
			 scopetable      ↓
			 trylevel
			 ebp
             .....           ↓

	↓	←	 Next   ←    ←   ←
			 Handle (异常处理函数) -> _except_handler3
			 scopetable
			 trylevel
			 ebp
	↓		 ....

			 -1 (0xFFFFFFFF)               (最后一个)
	→ 	→ 	 Handle  (异常处理函数)

scopetable_entry:

scopetable_entry结构体:

struct scopetable_entry
{
	DWORD previousTryLevel;  //上一个try{}结构编号(嵌套的第几个)
	PDWRD lpfnFitter;        //过滤函数的起始地址
	PDWRD lpfnHandler;       //异常处理程序的地址
}

当一个函数中具有多个try-except结构时,scopetable_entry结构体也会有多个,scopetable实际上是一个数组:

scopetable[0].previousTryLevel = -1; (无嵌套)
scopetable[0].lpfnFitter = 过滤函数1;
scopetable[0].lpfnHandler = 异常处理程序1;

scopetable[1].previousTryLevel = -1; (无嵌套)
scopetable[1].lpfnFitter = 过滤函数2;
scopetable[1].lpfnHandler = 异常处理程序2;

scopetable[2].previousTryLevel = 1; (嵌套在第2个异常中)
scopetable[2].lpfnFitter = 过滤函数3;
scopetable[2].lpfnHandler = 异常处理程序3;

编译器通过 previousTryLevel 这个值来判断你是否嵌套或者嵌套在哪一层。

trylevel:

标识当前代码正执行在哪一个try结构中。

__except_handler4 执行过程

__except_handler4 执行过程:

1)CPU检测到异常后,查中断表执行处理函数,CommonDispatchException,KiDispatchException,KiUserExceptionDisparcher,RtlDispatchException,VEH,SEH (自己抛出的异常只有前三个函数不同)

2)执行\_\_except\_handler3函数

	<1> 根据trylevel选择scopetable数组

	<2> 调用scopetable数组中对应的lpfnFilter函数

		1)EXCEPTION_EXECUTE_HANDLER (1) 执行 excrpt 代码
	
		2)EXCEPTION_CONTINUE_SEARCH (0) 寻找下一个异常处理函数
	
		3)EXCEPTION_CONTINUE_EXECUTION (-1) 返回出错位置重新执行


	<3> 如果lpfnFitter函数返回0,向上遍历直到 previousTryLevel = -1 这个嵌套异常或单个异常结束。

即一旦发生异常后,__except_handler4函数就已经接管了异常,由lpfnFilter函数的返回值决定之后的执行流程。

try-finally

try-finally语法:

__try
{
	//可能会出错的代码
}
__finally
{
	//一定会执行的代码
}

在finally中的代码是一定会执行的,例:

void TestTryFinally()
{
	for (int i = 0; i < 10; i++)
	{
		_try
		{
			continue;
			break;
			return;
		}
		__finally
		{
			printf("一定会执行的代码!");
		}
	}
}

不管是用 continue、break、return 结束或者跳出循环时,__finally中的代码一定会执行,在函数发生异常的时候也会调用。

try-finally原理分析

当调用return时的反汇编代码:

void TestTryFinally()
{
00FCDCD0  push        ebp  
00FCDCD1  mov         ebp,esp 

//下面是向异常链表保存一个结构体
00FCDCD3  push        0FFFFFFFEh 
00FCDCD5  push        offset ___rtc_tzz+168h (147BD38h) 
00FCDCDA  push        offset @ILT+29830(__except_handler4) (0FAE48Bh)  
00FCDCDF  mov         eax,dword ptr fs:[00000000h] 
00FCDCE5  push        eax  
00FCDCE6  add         esp,0FFFFFF38h 
00FCDCEC  push        ebx  
00FCDCED  push        esi  
00FCDCEE  push        edi  
00FCDCEF  lea         edi,[ebp-0D8h] 
00FCDCF5  mov         ecx,30h 
00FCDCFA  mov         eax,0CCCCCCCCh 
00FCDCFF  rep stos    dword ptr es:[edi] 
00FCDD01  mov         eax,dword ptr [___security_cookie (14ABC4Ch)] 
00FCDD06  xor         dword ptr [ebp-8],eax 
00FCDD09  xor         eax,ebp 
00FCDD0B  push        eax  
00FCDD0C  lea         eax,[ebp-10h] 
00FCDD0F  mov         dword ptr fs:[00000000h],eax 
	_try
00FCDD15  mov         dword ptr [ebp-4],0 
	{
		return;
00FCDD1C  push        0FFFFFFFEh 
00FCDD1E  lea         eax,[ebp-10h] 
00FCDD21  push        eax  
00FCDD22  push        offset ___security_cookie (14ABC4Ch) 
00FCDD27  call        @ILT+65015(__local_unwind4) (0FB6DFCh)   //在这发生扩展,去调用 finally 中的代码
00FCDD2C  add         esp,0Ch 
00FCDD2F  jmp         $LN8 (0FCDD4Dh)     //returen
	}
	__finally
00FCDD31  mov         dword ptr [ebp-4],0FFFFFFFEh 
00FCDD38  call        $LN5 (0FCDD3Fh) 
00FCDD3D  jmp         $LN8 (0FCDD4Dh) 
	{
		printf("一定会执行的代码!");
00FCDD3F  push        offset string "\xd2\xbb\xb6\xa8\xbb\xe1\xd6\xb4\xd0\xd0\xb5\xc4\xb4\xfa\xc2\xeb\xa3\xa1" (140072Ch) 
00FCDD44  call        @ILT+36690(_printf) (0FAFF57h) 
00FCDD49  add         esp,4 
$LN6:
00FCDD4C  ret              
	}

}

在_EXCEPTION_REGISTRION.handler 位置为NULL时,则说明当前异常记录是 try-finally 形式的。

局部展开:

当 try-finally 中 try代码提前结束时会产生,如Contiue、Break、Return等。

全局展开:

执行 except 代码之前会重新从异常发生位置遍历一次 finally,如果存在则一次调用局部展开函数:

 	void TestTryFinally()
	{
			_try
			{
				_try
				{
					*(int) 0 = 1;
				}
				__finally
				{
					printf("一定会执行的代码!");
				}

			}
			_except(1)     
			{
				printf("处理异常!");
			}
	}

当中断代码发生异常时,即给0地址赋值,这是会发生全局展开,此时在调用 except前会先重新从异常发生位置遍历一次 finally后在执行except。

顶层异常处理

顶层异常处理是基于SEH和线程的,但他的有效范围是整个进程。

在执行main函数(主线程)前进程会先执行一些函数(BaseProcessStart),在执行这些函数时,会先插入一个SEH(_SEH_prolog函数),所以说不存在无法找到SEH的情况。

当我们新建一个线程时依旧会先插入一个SEH(_SEH_prolog函数),在 BaseThreadStart中调用。

BaseProcessStart(伪代码):

void __stdcall BaseProcessStart(PVOID ThreadStartAddress)
{
	DWORD dwExitCode = 0;

	__try
	{
		//设置ETHREAD->Win32StartAddress为线程实际的起始地址
		NtSetInformationThread(NtCurrentThread(),
								ThreadQuerySetWin32StartAddress,
								&ThreadStartAddress,
								sizeof(ULONG_PTR)
								);
		//执行主线程函数,即exe入口函数
		dwExitCode = ThreadStartAddress();

		//退出线程
		ExitThread(dwExitCode);
	}
	__except(UnhandledExceptionFilter(GetExceptionInformation()))
	{
		if (BaseRunningInserverProcess)
		{
			ExitThread(GetExceptionCode()); //结束线程
		}
		else
		{
			ExitProcess(GetExceptionCode()); //结束线程
		}
	}
}

顶层异常处理函数 UnhandledExcrptionFilter函数执行流程:

UnhandledExcrptionFilter函数太长自行IDA,函数在Kernel32.dll中。(下面的时一部分,加密与解密书中有详解)

1)通过 NtQueryInformationProcess 查询当前进程是否正在被调试,如果是,返回EXCEPTION_CONTINUE_SEARCH(寻找下一个异常处理函数),此时会进入第二轮分发。(通过进程的DebugPort字段的值确定,即NtQueryInformationProcess查找的就是这个值)

2)如果没有被调试:

	<1> 查询是否通过 SetUnhandledExcptionFitter 注册处理函数,如果有就调用

	<2> 如果没有通过 SetUnhandledExcptionFitter 注册处理函数,弹出窗口,让用户选择终止程序韩式启动即时调试器

	<3> 如果用户没有启动即时调试器,那么该函数返回 EXCEPTION_EXECUTE_HANDLER (1) 执行 excrpt 代码,即结束程序。

通过以上方式实现反调试:

long __stdcall callback(_EXCEPTION_POINTERS *excp)
{
	excp->ContextRecord->Ecx = 1;
	return EXCEPTION_CONTINUE_EXECUTION;
}

int main()
{
	setUnhandledExceptionFilter(callback); //挂如异常处理函数
	
	//添加除零异常
	
	__asm
	{
		xor edx, edx
	    xor ecx, ecx
		mov eax, 0x10
		idiv ecx     //eax除ecx,发生异常
	}
	
	//程序正常执行
	
	print(L"程序正常执行!");
}

当代码正常执行时,触发了除零异常,当SEH没有其他异常时调用最后一道防线 UnhandledExcrptionFilter函数,在UnhandledExcrptionFilter函数中先判断是否存在调试器,因为是正常执行没有调用调试器,则判断是否存在 setUnhandledExceptionFilter函数注册的处理异常函数,此时我们已经注册过了,所以会调用这个处理异常函数(callback),这个函数将ECX修改为正常值后继续执行,所以后面的程序正常运行。

当代码采用调试器执行时,因为触发了除零异常,所以程序依旧调用 UnhandledExcrptionFilter函数,但是 UnhandledExcrptionFilter函数判断程序调用了调试器,所以并不会去调用 SetUnhandledExcptionFitter注册的处理异常函数,当程序进行2次分发后依旧没有发现能处理当前异常的方式后,程序结束。实现了反调试,因为只有程序在不加载调试器的情况下程序才能正常执行。

SetUnhandledExcptionFitter函数:

为了在 UnhandledExcrptionFilter阶段给用户一个干预的机会,所以微软提供了一个API函数 SetUnhandledExcptionFitter函数。用户通过SetUnhandledExcptionFitter函数设置一个顶层异常过滤函数,在 Kernerl32!UnhandledExceptionFilter中会调用它并根据它的返回值进行相应的操作,即“顶层异常回调函数”。

    long __stdcall callback(_EXCEPTION_POINTERS *excp)
	{
		excp->ContextRecord->Ecx = 1;
		return EXCEPTION_CONTINUE_EXECUTION;
	}

SetUnhandledExcptionFitter函数实际上把用户设置的回调函数地址加密保存在一个全局变量 Kernerl32!BasepCurrentTopLevelFilter中,即:

1)不管调用这个API多少次,只有最后一次设置的结果才是有效的,所以在同一时刻每个进程只可能有一个有效的顶层回调函数。

2)因为系统在创建用户线程时总会安装顶层异常处理过程,并把UnhandledExcrptionFilter函数作为异常过滤函数,所以该全局变量不仅对所有已经创建了的线程有效,对那些尚未“出生”的线程同样有效。

windows Vista:

从 windows Vista 开始,线程的实际入口点变成了 ntdll!RtlUserThreadStart。

顶层异常应用:

当发生了无法解决的异常后,通过顶层异常函数生成一个Dump文件,用来记录异常。

总结

异常调用流程(模拟和CPU):

	CPU异常:                                   模拟异常:

CPU指令检测到异常 (如除零)                CxxThrowException   //每个语言可能不一样
		↓                                          ↓
查IDT表,执行中断处理函数                   (Kernel32.dll) RaiseException  //构建_EXCEPTION_RECORD)
		↓                                          ↓
CommonDispatchException                   ntdll.dll !RtlRaiseException
(构建_EXCEPTION_RECORD)
		↓                                           ↓ 
										  ntdll.dll NT!NtRaiseException
		↓											↓
										  ntdll.dll NT!KiRaiseException
		↓                                           ↓
		
		↓                                           ↓
	-------------------KiDispatchException---------------------------  最后都调用这个函数分发异常(分发给用户或者内核)
												    
        ↓  (用户) 									↓  (内核)                                         
													
	  返回3环								     直接处理
		
		↓			
						   
	调用	KiUserExceptionDispatcher 	

		↓			

	KiDispatchException(函数中处理异常)
					
		↓

	   VEH  → VEH可以处理则直接处理
		
		↓  VEH不能处理

	   SEH  → SEH可以处理则直接处理

		↓  SEH不能处理

	UnhandledExcrptionFilter函数 (最后的顶层异常) → 存在调试器则交给调试器

		↓  没有调试器

	调用SetUnhandledExcptionFitter注册的顶层异常回调函数 → 存在,根据我们设置执行代码(用户定义的)

		↓  不存在注册的顶层异常回调函数
	
	提示窗口结束进程

蓝色妖姬 – 相守