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注册的顶层异常回调函数 → 存在,根据我们设置执行代码(用户定义的)
↓ 不存在注册的顶层异常回调函数
提示窗口结束进程