首页
论坛
课程
招聘
[原创]Windows内核学习笔记之异常(上)
2021-12-30 12:14 20038

[原创]Windows内核学习笔记之异常(上)

2021-12-30 12:14
20038

一.中断和异常

1.概念

中断通常是由CPU外部的输入输出设备(硬件)所触发的,供外部设备通知CPU”有事情要处理“,因此又称为中断请求。中断请求的目的是希望CPU暂时停止执行当前执行的程序,转去执行中断请求所对应的中断处理例程。

 

与中断不同,异常通常是CPU在执行指令时,即当CPU在执行程序指令时遇到操作数有错误或检测到指令规范中定义的非法情况。前者的一个典型例子是执行除法指令时遇到除数为0,后者的典型例子包括在用户模式下执行特权指令等。第二种来源是某些特殊指令,这些指令的预期行为就是产生相应的异常,比如INT 3指令的目的就是产生一个断点异常,让CPU中断进调试器。换句话说,这个异常是”故意“产生的,是预料内的。这样的指令还有INTO,INT O和BOUND。第三种来源是奔腾CPU引入的机器检查异常,即当CPU执行指令期间检测到CPU内部或外部的硬件错误。

 

中断与异常的根本差异是:异常来自于CPU本身,是CPU主动产生的;而中断则是来源于外部设备,是中断源发起的,CPU是被动的。

2.异常的分类

根据CPU报告异常的方式和导致异常的指令是否可以安全地重新执行,IA-32 CPU把异常分为3类:错误(fault),陷阱(trap)和中止(abort)。

A.错误类异常

导致错误类异常地情况通常可以被纠正,而且一旦纠正以后,程序可以无损失地恢复执行,此类异常最常见地一个例子就是缺页异常。

 

当CPU报告错误类异常时,CPU将其恢复成导致该异常的指令被执行之前的状态。而且在CPU转去执行异常处理程序前,在栈钟保存的CS和EIP指针是指向导致异常的这条指令(而不是下一条指令)。因此,当异常处理程序返回继续执行时,CPU接下来执行的第一条指令依然是刚才导致异常的那条指令。所以,如果导致异常的情况没有被消除,那么,CPU将会再次产生异常。

B.陷阱类异常

与错误类异常不同,当CPU报告陷阱类异常时,导致该异常的指令已经执行完毕,压入栈钟的CS和EIP值(即异常处理程序的返回地址)是导致该异常的指令执行后紧接着要执行的下一条指令。值得说明的是,下一条指令并不一定是与导致异常的指令相邻的下一条。如果导致异常的指令是跳转指令或函数调用指令,那么下一条指令可能是内存地址不相邻的另一条指令。

 

导致陷阱类异常的情况通常也是可以无损失地恢复执行的。比如INT 3指令导致的断点异常就属于陷阱类异常,该异常会使CPU中断到调试器,从调试器返回后,被调试器程序可以继续执行。

C.中止类异常

中止类异常主要是用来报告严重的错误,比如硬件错误和系统表中包含非法值或不一致的状态等。这类异常不允许恢复继续执行。原因有二:首先,当这类异常发生时,CPU并不总能保证报告的导致异常的指令地址是精确的。其次,出于安全性考虑,这类异常可能是由于导致该异常的程序执行非法操作导致的,因此就应该强迫其中止退出。

 

下表是这三类异常的关键特征:

分类 报告时间 保存的CS和EIP指针 可恢复性
错误 开始执行导致异常的指令时 导致异常的那条指令 可以恢复执行
陷阱 执行完导致异常的指令时 导致异常的那他指令的下一条指令 可以恢复执行
中止 不确定 不确定 不可以

3.中断描述符表

在保护模式下,当有中断或异常发生时,CPU是通过中断描述符表(IDT)来寻找处理函数的。因此,可以说IDT是CPU(硬件)与操作系统(软件)交接的中断和异常的关口。操作系统在启动早期的一个重要任务就是设置IDT,准备好处理异常和中断的各个函数。每个中断或异常都被赋予了一个整数ID,称为向量号。系统根据向量号从IDT表中查找相应的中断或异常的处理例程。IA-32架构规定0~31号向量供CPU设计者使用,32~255号向量(224个)供操作系统和计算机系统生产厂商或其他软硬件开发商使用。下表归纳了PC系统中常见向量号对应的中断和异常:

向量号 助记符 门类型/类型 处理例程/TSS选择子 中断/异常 来源
00 #DE 中断 nt!KiTrap00 除0错误 DIV和IDIV指令
01 #DB 中断 nt!KiTrap01 调试异常,用于软件调试 任何代码或数据的引用
02 任务 0x0058 NMI中断 不可屏蔽的外部中断
03 #BP 中断 nt!KiTrap03 断点 INT 3指令
04 #OF 中断 nt!KiTrap04 溢出 INTO指令
05 #BR 中断 nt!KiTrap05 数组越界 BOUND指令
06 #UD 中断 nt!KiTrap06 无效指令 UD2指令或任何保留的指令
07 #NM 中断 nt!KiTrap07 数学协处理器不存在或不可用 浮点或WAIT/FWAIT指令
08 #DF 任务 0x0050 双重错误 任何可能产生异常的指令,不可屏蔽中断或可屏蔽中断
09 #MF 中断 nt!KiTrap09 协处理器段溢出 浮点指令
0A #TS 中断 nt!KiTrap0A 无效TSS 任务切换或访问TSS
0B #NP 错误 nt!KiTrap0B 段不存在 加载段寄存器或访问系统段
0C #SS 错误 nt!KiTrap0C 栈段错误 栈操作或加载SS寄存器
0D #GP 错误 nt!KiTrap0D 一般性保护 任何内存引用和保护性检查
0E #PF 错误 nt!KiTrap0E 页错误 任何内存引用
0F 保留 nt!KiTrap0F
10 #MF 错误 nt!KiTrap10 浮点错误 浮点或WAIT/FWAIT指令
11 #AC 错误 nt!KiTrap11 内存对齐检查 对内存中数据的引用
12 #MC 中止 nt!KiTrap12 机器检查 错误代码和来源于型号有关
13 #XF 错误 nt!KiTrap13 SIMD浮点错误 SIMD浮点指令
14~1F 保留 nt!KiTrap0F
20~28 保留 NULL 未使用
29 nt!KiRaiseSecurityCheckFailure 异常 用于支持Windows8引入的FailFast机制
2A nt!KiGetTickCount
2B nt!KiCallbackReturn 从逆向调用返回
2C nt!KiRaiseAssertion 断言
2D nt!KiDebugService 调试服务
2E nt!KiSystemService 系统服务
2F nt!KiTrap0F
30 hal!Halp8254ClockInterrupt IRQ0 时钟中断
31~3F 驱动程序通过KINTERRUPT结构注册的处理例程 IRQ1~IRQ15 其他硬件设备的中断
40~FD nt!KiUnexpectedInterruptX N/A 没有使用

4.中断/异常的优先级

CPU在同一时间只能执行一个程序,如果多个中断请求或异常情况同时发生,CPU就会按照优先级高低次序依次处理,先处理优先级最高的。下表是IA-32架构定义的10个中断/异常优先级别:

优先级 描述
1(最高) 硬件重启动和机器检查异常
2 任务切换陷阱
3 外部硬件(例如芯片组)通过CPU引脚发给CPU的特别干预
4 上一条指令导致的陷阱:执行INT3导致的断电;调试陷阱,包括单步执行异常(EFLAGS[TF]=1)和利用调试寄存器设置的数据或输入输出断点
5 不可屏蔽(外部硬件)中断(NMI)
6 可屏蔽(外部硬件)中断
7 代码断点错误异常,即从内存取指令时检测到于调试寄存器中的断点地址相匹配,也就是利用调试寄存器设置的代码断点
8 取下一条指令时检测到的错误:违法代码段长度限制;代码内存页错误(即代码属性的内存页导致的错误)
9 解码下一指令时检测到的错误:指令长度大于15字节(包括前缀);非法操作码;协处理器不可用
10(最低) 指令指令时检测到的错误:溢出,当EFLAGS[OF]=1时执行INTO指令;执行BOUND指令时检测到边界错误;无效TSS;段不存在;栈异常;一般保护异常;数据页错误;对齐检查异常;x87 FPU异常;SIMD浮点异常

二.异常的描述和登记

1.异常的描述

在操作系统层次有两类异常:

  • CPU异常(硬件异常):由CPU产生的

  • 软件异常:通过软件方式模拟出的异常,比如调用RaiseException API而产生的异常和使用编程语言的throw关键字抛出的异常

无论是哪类异常,Windows操作系统都用EXCEPTION_RECORD结构来描述它,该结构的定义如下:

1
2
3
4
5
6
7
8
9
10
#define EXCEPTION_MAXIMUM_PARAMETERS 15 // maximum number of exception parameters
 
typedef struct _EXCEPTION_RECORD {
    DWORD    ExceptionCode;
    DWORD ExceptionFlags;
    struct _EXCEPTION_RECORD *ExceptionRecord;
    PVOID ExceptionAddress;
    DWORD NumberParameters;
    ULONG_PTR ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];
    } EXCEPTION_RECORD;
名称 作用
ExceptionCode 异常代码:一个32位的整数,其格式是Windows系统的状态代码格式
ExceptionFlags 异常标志:它的每一位代表一种标志
ExceptionRecord 相关的另一个异常:EXCEPTION_RECORD指针,指向与该异常相关的另一个异常记录,如果没有相关的异常,那么这个指针就会为空
ExceptionAddress 异常发生地址:记录异常地址,对于硬件异常,它的值因为异常类型不同而可能是导致异常的那条指令的地址,或者是导致异常指令的下一条地址
NumberParameters 参数数组中元素个数:ExceptionInformation数组中包含的有效参数个数,该结构允许存储15个附加参数
ExceptionInformation 参数数组
 

对于ExceptionFlags标志位,目前已定义的标志位如下:

  • EH_NONCONTINUABLE(1):该异常不可恢复继续执行

  • EH_UNWINDING(2):当因为执行栈展开而调用异常处理函数时,会设置此标志

  • EH_EXIT_UNWIND(4):也是用于栈展开,较少使用

  • EH_STACK_INVALID(8):当检测到栈错误时,设置此标志

  • EH_ENSTED_CALL(0x10):用于表示内嵌的异常

下图则是ExceptionCode对应的用于异常的状态码:

 

avatar

2.登记CPU异常

对于产生的CPU异常,处理器会根据IDT表查询到需要执行的函数,如下所示是部分产生了CPU异常以后会执行的处理例程:

 

avatar

 

以除0异常调用的KiTrap0为例,函数会首先对陷阱帧进行填充,然后对cs寄存器进行判断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
.text:00407522 _KiTrap00       proc near               ; DATA XREF: INIT:_IDT↓o
.text:00407522
.text:00407522 var_2           = word ptr -2
.text:00407522 arg_4           = dword ptr  8
.text:00407522
.text:00407522 ; FUNCTION CHUNK AT .text:00407399 SIZE 00000021 BYTES
.text:00407522
.text:00407522                 push    0               ; 填充ErrorCode
.text:00407524                 mov     [esp+4+var_2], 0
.text:0040752B                 push    ebp             ; 填充Ebp
.text:0040752C                 push    ebx             ; 填充Ebx
.text:0040752D                 push    esi             ; 填充Esi
.text:0040752E                 push    edi             ; 填充Edi
.text:0040752F                 push    fs              ; 填充fs
.text:00407531                 mov     ebx, 30h
.text:00407536                 mov     fs, ebx         ; 将fs指向KPCR
.text:00407538                 assume fs:nothing
.text:00407538                 mov     ebx, large fs:0
.text:0040753F                 push    ebx             ; 填充ExceptionList
.text:00407540                 sub     esp, 4          ; esp指向Eax
.text:00407543                 push    eax             ; 填充Eax
.text:00407544                 push    ecx             ; 填充Ecx
.text:00407545                 push    edx             ; 填充Edx
.text:00407546                 push    ds              ; 填充ds
.text:00407547                 push    es              ; 填充es
.text:00407548                 push    gs              ; 填充gs
.text:0040754A                 mov     ax, 23h
.text:0040754E                 sub     esp, 30h        ; 将esp指向陷阱帧头部,也就是DbgEbp
.text:00407551                 mov     ds, eax
.text:00407553                 assume ds:nothing
.text:00407553                 mov     es, eax         ; 将ds, es赋值为0x23
.text:00407555                 assume es:nothing
.text:00407555                 mov     ebp, esp        ; 将ebp指向陷阱帧头部
.text:00407557                 test    [esp+68h+arg_4], 20000h ; 判断是否为虚拟8086模式
.text:0040755F                 jnz     short V86_kit0_a
.text:00407561
.text:00407561 loc_407561:                             ; CODE XREF: V86_kit0_a+25↑j
.text:00407561                 cld
.text:00407562                 mov     ebx, [ebp+60h]  ; 将ebp赋给ebx
.text:00407565                 mov     edi, [ebp+68h]  ; 将Eip赋给edi
.text:00407568                 mov     [ebp+0Ch], edx  ; 将edx赋给DbgArgPointer
.text:0040756B                 mov     dword ptr [ebp+8], 0BADB0D00h ; 为DbgArgMark赋值
.text:00407572                 mov     [ebp+0], ebx    ; 填充DbgEbp
.text:00407575                 mov     [ebp+4], edi    ; 填充DbgEip
.text:00407578                 test    large byte ptr fs:50h, 0FFh ; 判断是否处于调试模式
.text:00407580                 jnz     Dr_kit0_a
.text:00407586
.text:00407586 loc_407586:                             ; CODE XREF: Dr_kit0_a+10↑j
.text:00407586                                         ; Dr_kit0_a+7C↑j
.text:00407586                 test    dword ptr [ebp+70h], 20000h ; 判断是否处于虚拟8086模式
.text:0040758D                 jnz     short loc_4075CC
.text:0040758F                 test    byte ptr [ebp+6Ch], 1 ; 判断cs寄存器第0位是否有1
.text:00407593                 jz      short loc_40759C
.text:00407595                 cmp     word ptr [ebp+6Ch], 1Bh ; 判断cs寄存器是否等于0x1B
.text:0040759A                 jnz     short loc_4075B9

接着对进程的成员进行判断,最终都会跳到loc_5074AB执行

1
2
3
4
5
6
7
8
9
10
11
12
13
.text:004075B9 loc_4075B9:                             ; CODE XREF: _KiTrap00+78↑j
.text:004075B9                 mov     ebx, large fs:124h
.text:004075C0                 mov     ebx, [ebx+_KTHREAD.ApcState.Process]
.text:004075C3                 cmp     [ebx+_EPROCESS.VdmObjects], 0
.text:004075CA                 jz      short loc_4075AB
.text:004075CC
.text:004075CC loc_4075CC:                             ; CODE XREF: _KiTrap00+6B↑j
.text:004075CC                 push    0
.text:004075CE                 call    _Ki386VdmReflectException_A@4 ; Ki386VdmReflectException_A(x)
.text:004075D3                 or      al, al
.text:004075D5                 jnz     Kei386EoiHelper@0 ; Kei386EoiHelper()
.text:004075DB                 jmp     short loc_4075AB
.text:004075DB _KiTrap00       endp

loc_4075AB处的代码会对ebx和eax分别赋值为Eip和异常状态码,然后跳转到loc_407399

1
2
3
4
5
6
.text:004075AB loc_4075AB:                             ; CODE XREF: _KiTrap00+A8↓j
.text:004075AB                                         ; _KiTrap00+B9↓j
.text:004075AB                 sti
.text:004075AC                 mov     ebx, [ebp+68h]  ; 将陷阱帧中的Eip赋值给ebx
.text:004075AF                 mov     eax, STATUS_INTEGER_DIVIDE_BY_ZERO
.text:004075B4                 jmp     loc_407399

而loc_407399的代码仅仅是清空ecx,此时的ecx保存的其实是参数的个数,由于除0异常无需传参,所以ecx为0,而将其他信息作为附带参数(最多3个)的时候,会分别将参数放入EDX(参数一), ESI(参数二), EDI(参数三), 接着调用CommonDispatchException函数

1
2
3
4
.text:00407399 loc_407399:                             ; CODE XREF: _KiTrap00+84↓j
.text:00407399                                         ; _KiTrap00+92↓j ...
.text:00407399                 xor     ecx, ecx
.text:0040739B                 call    CommonDispatchException

CommonDispatchException会在栈上开辟空间,在根据传递的参数来为开辟的结构体EXCEPTION_RECORD赋值,随后调用KiDispatchException函数来完成异常的分发

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
.text:004073BA CommonDispatchException proc near       ; CODE XREF: _KiTrap00-187↑p
.text:004073BA                                         ; _KiTrap00-17B↑p ...
.text:004073BA
.text:004073BA var_50          = _EXCEPTION_RECORD ptr -50h
.text:004073BA
.text:004073BA                 sub     esp, 50h
.text:004073BD                 mov     [esp+50h+var_50.ExceptionCode], eax
.text:004073C0                 xor     eax, eax
.text:004073C2                 mov     [esp+50h+var_50.ExceptionFlags], eax
.text:004073C6                 mov     [esp+50h+var_50.ExceptionRecord], eax
.text:004073CA                 mov     [esp+50h+var_50.ExceptionAddress], ebx
.text:004073CE                 mov     [esp+50h+var_50.NumberParameters], ecx
.text:004073D2                 cmp     ecx, 0          ; 判断是否有参数
.text:004073D5                 jz      short loc_4073E3 ; 将ExceptionRecord地址赋给ecx
.text:004073D7                 lea     ebx, [esp+50h+var_50.ExceptionInformation]
.text:004073DB                 mov     [ebx], edx
.text:004073DD                 mov     [ebx+4], esi
.text:004073E0                 mov     [ebx+8], edi
.text:004073E3
.text:004073E3 loc_4073E3:                             ; CODE XREF: CommonDispatchException+1B↑j
.text:004073E3                 mov     ecx, esp        ; 将ExceptionRecord地址赋给ecx
.text:004073E5                 test    dword ptr [ebp+70h], 20000h
.text:004073EC                 jz      short loc_4073F5
.text:004073EE                 mov     eax, 0FFFFh
.text:004073F3                 jmp     short loc_4073F8
.text:004073F5 ; ---------------------------------------------------------------------------
.text:004073F5
.text:004073F5 loc_4073F5:                             ; CODE XREF: CommonDispatchException+32↑j
.text:004073F5                 mov     eax, [ebp+6Ch]
.text:004073F8
.text:004073F8 loc_4073F8:                             ; CODE XREF: CommonDispatchException+39↑j
.text:004073F8                 and     eax, 1
.text:004073FB                 push    1               ; FirstChance
.text:004073FD                 push    eax             ; PreviousMode
.text:004073FE                 push    ebp             ; TrapFrame
.text:004073FF                 push    0               ; ExceptionFrame
.text:00407401                 push    ecx             ; ExceptionRecord
.text:00407402                 call    _KiDispatchException@20 ; KiDispatchException(x,x,x,x,x)
.text:00407407                 mov     esp, ebp
.text:00407409                 jmp     Kei386EoiHelper@0 ; Kei386EoiHelper()
.text:00407409 CommonDispatchException endp

3.登记软件异常

软件异常的实现是需要取决于编译器的,对于vs2017来说,可以使用throw关键字来产生软件异常,该关键字是通过调用_CxxThrowException函数来实现的

 

avatar

 

而_CxxThrowException函数则会调用kernel32.dll中的RaiseException,此时的传入的第一个参数在RaiseException中会赋给ExceptionCode,此时是0xE06D7363,该值取决于编译器,每次调用的时候都是这个数值

 

avatar

 

在RaiseException函数中会开辟栈空间用来保存EXCEPTION_RECORD结构,异常发生的地址ExceptionAddress则固定为_RaiseException,而不是异常发生地址。随后调用ntdll.dll中的RtlRaiseException,该函数会将执行上下文(通用寄存器等)放入CONTEXT结构,然后通过系统服务调用机制调用内核函数KiRaiseException

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
.text:7C812AA9 ; void __stdcall RaiseException(DWORD dwExceptionCode, DWORD dwExceptionFlags, DWORD nNumberOfArguments, const ULONG_PTR *lpArguments)
.text:7C812AA9                 public _RaiseException@16
.text:7C812AA9 _RaiseException@16 proc near            ; CODE XREF: OutputDebugStringA(x)+4F↓p
.text:7C812AA9                                         ; DATA XREF: .text:off_7C802654↑o ...
.text:7C812AA9
.text:7C812AA9 ExceptionRecord = EXCEPTION_RECORD ptr -50h
.text:7C812AA9 dwExceptionCode = dword ptr  8
.text:7C812AA9 dwExceptionFlags= dword ptr  0Ch
.text:7C812AA9 nNumberOfArguments= dword ptr  10h
.text:7C812AA9 lpArguments     = dword ptr  14h
.text:7C812AA9 arg_14          = word ptr  1Ch
.text:7C812AA9 arg_18          = dword ptr  20h
.text:7C812AA9
.text:7C812AA9 ; FUNCTION CHUNK AT .text:7C844950 SIZE 00000008 BYTES
.text:7C812AA9 ; FUNCTION CHUNK AT .text:7C84B737 SIZE 00000037 BYTES
.text:7C812AA9
.text:7C812AA9                 mov     edi, edi
.text:7C812AAB                 push    ebp
.text:7C812AAC                 mov     ebp, esp
.text:7C812AAE                 sub     esp, 50h        ; 开辟栈空间
.text:7C812AB1                 mov     eax, [ebp+dwExceptionCode] ; 将第一个参数赋给eax
.text:7C812AB4                 and     [ebp+ExceptionRecord.ExceptionRecord], 0
.text:7C812AB8                 mov     [ebp+ExceptionRecord.ExceptionCode], eax ; 为ExceptionCode赋值
.text:7C812ABB                 mov     eax, [ebp+dwExceptionFlags]
.text:7C812ABE                 push    esi
.text:7C812ABF                 mov     esi, [ebp+lpArguments]
.text:7C812AC2                 and     eax, 1
.text:7C812AC5                 test    esi, esi
.text:7C812AC7                 mov     [ebp+ExceptionRecord.ExceptionFlags], eax
.text:7C812ACA                 mov     [ebp+ExceptionRecord.ExceptionAddress], offset _RaiseException@16 ; RaiseException(x,x,x,x)
.text:7C812AD1                 jz      loc_7C812B70
.text:7C812AD7                 mov     ecx, [ebp+nNumberOfArguments]
.text:7C812ADA                 cmp     ecx, 0Fh
.text:7C812ADD                 ja      loc_7C844950
.text:7C812AE3
.text:7C812AE3 loc_7C812AE3:                           ; CODE XREF: RaiseException(x,x,x,x)+31EAA↓j
.text:7C812AE3                 test    ecx, ecx        ; 参数个数是否为0
.text:7C812AE5                 mov     [ebp+ExceptionRecord.NumberParameters], ecx
.text:7C812AE8                 jz      short loc_7C812AF1
.text:7C812AEA                 push    edi
.text:7C812AEB                 lea     edi, [ebp+ExceptionRecord.ExceptionInformation]
.text:7C812AEE                 rep movsd
.text:7C812AF0                 pop     edi
.text:7C812AF1
.text:7C812AF1 loc_7C812AF1:                           ; CODE XREF: RaiseException(x,x,x,x)+3F↑j
.text:7C812AF1                                         ; RaiseException(x,x,x,x)+CB↓j
.text:7C812AF1                 lea     eax, [ebp+ExceptionRecord]
.text:7C812AF4                 push    eax             ; ExceptionRecord
.text:7C812AF5                 call    ds:__imp__RtlRaiseException@4 ; RtlRaiseException(x)
.text:7C812AFB                 pop     esi
.text:7C812AFC                 leave
.text:7C812AFD                 retn    10h

NtRaiseException函数会继续调用KiRaiseException

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
.text:0040A1DF ; NTSTATUS __stdcall NtRaiseException(PEXCEPTION_RECORD ExceptionRecord, PCONTEXT ContextRecord, BOOLEAN FirstChance)
.text:0040A1DF _NtRaiseException@12 proc near          ; DATA XREF: .text:0040DAF4↓o
.text:0040A1DF
.text:0040A1DF TraFrame        = dword ptr  0
.text:0040A1DF ExceptionRecord = dword ptr  8
.text:0040A1DF ContextRecord   = dword ptr  0Ch
.text:0040A1DF FirstChance     = byte ptr  10h
.text:0040A1DF TrapFrame       = dword ptr  3Ch
.text:0040A1DF
.text:0040A1DF                 push    ebp
.text:0040A1E0                 mov     ebx, large fs:124h
.text:0040A1E7                 mov     edx, [ebp+TrapFrame]
.text:0040A1EA                 mov     [ebx+_KTHREAD.TrapFrame], edx
.text:0040A1F0                 mov     ebp, esp
.text:0040A1F2                 mov     ebx, [ebp+TraFrame]
.text:0040A1F5                 mov     edx, dword ptr [ebp+FirstChance]
.text:0040A1F8                 mov     eax, [ebx+_ETHREAD.Tcb.ContextSwitches]
.text:0040A1FB                 mov     ecx, [ebp+ContextRecord]
.text:0040A1FE                 mov     large fs:0, eax
.text:0040A204                 mov     eax, [ebp+ExceptionRecord]
.text:0040A207                 push    edx             ; FirstChance
.text:0040A208                 push    ebx             ; TrapFrame
.text:0040A209                 push    0               ; ExceptionFrame
.text:0040A20B                 push    ecx             ; ContextRecord
.text:0040A20C                 push    eax             ; ExceptionRecord
.text:0040A20D                 call    _KiRaiseException@20 ; KiRaiseException(x,x,x,x,x)
.text:0040A212                 pop     ebp
.text:0040A213                 mov     esp, ebp
.text:0040A215                 or      eax, eax
.text:0040A217                 jz      _KiServiceExit2
.text:0040A21D                 jmp     _KiServiceExit
.text:0040A21D _NtRaiseException@12 endp

KiRaiseException会将CONTEXT保存到TrapFrame中,调用KiDispatchException来实现异常的分发

1
2
3
4
5
6
7
8
9
10
11
12
13
14
.text:004413DD                 push    dword ptr [ebp+PreviousMode]
.text:004413E3                 mov     eax, [ebp+var_ContextRecord]
.text:004413E9                 push    [eax+_CONTEXT.ContextFlags]
.text:004413EB                 push    eax
.text:004413EC                 push    [ebp+var_ExceptionFrame]
.text:004413F2                 push    [ebp+var_TrapFrame]
.text:004413F8                 call    _KeContextToKframes@20 ; KeContextToKframes(x,x,x,x,x)
.text:004413FD                 and     byte ptr [ebx+3], 0EFh ; 清空第4
.text:00441401                 push    dword ptr [ebp+FirstChance] ; FirstChance
.text:00441404                 push    dword ptr [ebp+PreviousMode] ; PreviousMode
.text:0044140A                 push    [ebp+var_TrapFrame] ; TrapFrame
.text:00441410                 push    [ebp+var_ExceptionFrame] ; ExceptionFrame
.text:00441416                 push    ebx             ; ExceptionRecord
.text:00441417                 call    _KiDispatchException@20

综上所述,无论是CPU异常还是软件异常,尽管产生的原因不同,但最终都会调用内核中的KiDispatchException来分发异常,也就是说,Windows系统是使用统一的方法来分发CPU异常和软件异常

三.异常的分发

1.异常分发函数

Windows内核中的KiDispatchException函数是分发各种Windows异常的枢纽。其函数原型如下:

1
2
3
4
5
VOID KiDispatchException(IN PEXCEPTION_RECORD ExceptionRecord,
                         IN PKEXCEPTION_FRAME ExceptionFrame,
                         IN PKTRAP_FRAME TrapFrame,
                         IN KPROCESSOR_MODE PreviousMode,
                         IN BOOLEAN FirstChance);
参数 含义
ExceptionRecord 指向EXCEPTION_RECORD结构,用来描述要分发的异常
ExceptionFrame 对于x86系统,该参数总是为NULL
TrapFrame 指向KTRAP_FRAME结构体,用来描述异常发生时的处理器状态
PreviousMode 指定先前模式,为0则先前模式为内核模式,为1则先前模式为用户模式
FirstChance 表示是否是第一轮分发这个异常。对于一个异常,Windows系统最多分发两轮
 

下图为KiDispatchException函数的执行流程,该函数首先调用KeContextFromKframes函数来将参数TrapFrame中的内容保存到CONTEXT中,以供向调试器和异常处理器函数报告异常时使用,接下来根据先前模式来选择操作流程

 

avatar

 

根据IDA反汇编的结果可以看到KiDispatchException首先对局部变量进行赋值,然后再根据先前模式以及是否存在调试器来修改CONTEXT结构中的ContextFlags

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
.text:004249F5 ; int __stdcall KiDispatchException(PEXCEPTION_RECORD ExceptionRecord, int ExceptionFrame, ULONG_PTR TrapFrame, KPROCESSOR_MODE PreviousMode, BOOLEAN FirstChance)
.text:004249F5 _KiDispatchException@20 proc near       ; CODE XREF: CommonDispatchException+48↑p
.text:004249F5                                         ; KiRaiseException(x,x,x,x,x)+158↓p ...
.text:004249F5
.text:004249F5 var_3A0         = dword ptr -3A0h
.text:004249F5 var_394         = dword ptr -394h
.text:004249F5 var_350         = EXCEPTION_RECORD ptr -350h
.text:004249F5 var_300         = dword ptr -300h
.text:004249F5 var_2FC         = dword ptr -2FCh
.text:004249F5 var_TrapFrame   = dword ptr -2F8h
.text:004249F5 var_2F4         = dword ptr -2F4h
.text:004249F5 var_ExceptionFrame= dword ptr -2F0h
.text:004249F5 var_ExceptionRecord= dword ptr -2ECh
.text:004249F5 Context         = CONTEXT ptr -2E8h
.text:004249F5 var_Cookie      = dword ptr -1Ch
.text:004249F5 ms_exc          = CPPEH_RECORD ptr -18h
.text:004249F5 ExceptionRecord = dword ptr  8
.text:004249F5 ExceptionFrame  = dword ptr  0Ch
.text:004249F5 TrapFrame       = dword ptr  10h
.text:004249F5 PreviousMode    = byte ptr  14h
.text:004249F5 FirstChance     = byte ptr  18h
.text:004249F5                 push    390h
.text:004249FA                 push    offset stru_424AF8
.text:004249FF                 call    __SEH_prolog
.text:00424A04                 mov     eax, ds:___security_cookie
.text:00424A09                 mov     [ebp+var_Cookie], eax
.text:00424A0C                 mov     esi, [ebp+ExceptionRecord]
.text:00424A0F                 mov     [ebp+var_ExceptionRecord], esi
.text:00424A15                 mov     ecx, [ebp+ExceptionFrame]
.text:00424A18                 mov     [ebp+var_ExceptionFrame], ecx
.text:00424A1E                 mov     ebx, [ebp+TrapFrame]
.text:00424A21                 mov     [ebp+var_TrapFrame], ebx
.text:00424A27                 mov     eax, large fs:20h
.text:00424A2D                 inc     [eax+_KPRCB.KeExceptionDispatchCount]
.text:00424A33                 mov     [ebp+Context.ContextFlags], 10017h
.text:00424A3D                 cmp     [ebp+PreviousMode], 1 ; 先前模式是否为用户模式
.text:00424A41                 jz      short loc_424A4C
.text:00424A43                 cmp     ds:_KdDebuggerEnabled, 0 ; 是否存在内核调试器
.text:00424A4A                 jz      short loc_424A69
.text:00424A4C
.text:00424A4C loc_424A4C:                             ; CODE XREF: KiDispatchException(x,x,x,x,x)+4C↑j
.text:00424A4C                 mov     [ebp+Context.ContextFlags], 1001Fh
.text:00424A56                 cmp     ds:_KeI386XMMIPresent, 0
.text:00424A5D                 jz      short loc_424A69
.text:00424A5F                 mov     [ebp+Context.ContextFlags], 1003Fh

将TrapFrame中的数据保存到Context中,判断是否为断点异常,如果是断点异常,将EIP减掉1

1
2
3
4
5
6
7
8
9
10
11
.text:00424A69 loc_424A69:                             ; CODE XREF: KiDispatchException(x,x,x,x,x)+55↑j
.text:00424A69                                         ; KiDispatchException(x,x,x,x,x)+68↑j
.text:00424A69                 lea     eax, [ebp+Context]
.text:00424A6F                 push    eax
.text:00424A70                 push    ecx
.text:00424A71                 push    ebx
.text:00424A72                 call    _KeContextFromKframes@12 ; KeContextFromKframes(x,x,x)
.text:00424A77                 mov     eax, [esi+_EXCEPTION_RECORD.ExceptionCode]
.text:00424A79                 cmp     eax, EXCEPTION_BREAKPOINT ; 判断是否为断点异常
.text:00424A7E                 jnz     loc_44110E
.text:00424A84                 dec     [ebp+Context._Eip]

如果不是断点异常就会根据异常代码选择是否为KI_EXCEPTION_ACCESS_VIOLATION来选择是否要将异常代码改成EXCEPTION_ACCESS_VIOLATION

1
2
3
4
5
6
.text:0044110E loc_44110E:                             ; CODE XREF: KiDispatchException(x,x,x,x,x)+89↑j
.text:0044110E                 cmp     eax, KI_EXCEPTION_ACCESS_VIOLATION
.text:00441113                 jnz     loc_424A8A      ; 清空edi
.text:00441119                 mov     [esi+_EXCEPTION_RECORD.ExceptionCode], EXCEPTION_ACCESS_VIOLATION
.text:0044111F                 cmp     [ebp+PreviousMode], 1
.text:00441123                 jnz     loc_424A8A

根据先前模式来选择执行流程

1
2
3
4
5
6
7
.text:00424A8A loc_424A8A:                             ; CODE XREF: KiDispatchException(x,x,x,x,x)+1C71E↓j
.text:00424A8A                                         ; KiDispatchException(x,x,x,x,x)+1C72E↓j ...
.text:00424A8A                 xor     edi, edi        ; 清空edi
.text:00424A8C
.text:00424A8C loc_424A8C:                             ; CODE XREF: KiDispatchException(x,x,x,x,x)+266BE↓j
.text:00424A8C                 cmp     [ebp+PreviousMode], 0 ; 判断先前模式是否是内核模式
.text:00424A90                 jnz     loc_440F9E

2.内核异常的分发

对于内核异常,首先会判断是否是第一次执行,如果是第一次执行就会继续判断是否存在内核调试器,如果存在内核调试器此时的KiDebugRoutine就会保存内核调试引擎的KdpTrap。如果该函数调用成功,返回值就会为1,否则会0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.text:00424A96                 cmp     [ebp+FirstChance], 1 ; 判断是否为第一次执行
.text:00424A9A                 jnz     loc_44B0C0
.text:00424AA0                 mov     eax, ds:_KiDebugRoutine
.text:00424AA5                 cmp     eax, edi        ; 判断是否存在内核调试器
.text:00424AA7                 jz      loc_4335A6
.text:00424AAD                 push    edi
.text:00424AAE                 push    edi
.text:00424AAF                 lea     ecx, [ebp+Context]
.text:00424AB5                 push    ecx
.text:00424AB6                 push    esi
.text:00424AB7                 push    [ebp+var_ExceptionFrame]
.text:00424ABD                 push    ebx
.text:00424ABE                 call    eax ; _KiDebugRoutine
.text:00424AC0                 test    al, al          ; 判断返回值是否为0
.text:00424AC2                 jz      loc_4335A6

无论是不存在内核调试器还是KdpTrap函数执行失败,都会跳转到loc_4335A6处执行,此时会调用RtlDispatchException函数来处理异常,如果函数执行成功就会返回1,否则返回0

1
2
3
4
5
6
7
.text:004335A6 loc_4335A6:                             ; CODE XREF: KiDispatchException(x,x,x,x,x)+B2↑j
.text:004335A6                                         ; KiDispatchException(x,x,x,x,x)+CD↑j
.text:004335A6                 lea     eax, [ebp+Context]
.text:004335AC                 push    eax             ; Context
.text:004335AD                 push    esi             ; ExceptionRecord
.text:004335AE                 call    _RtlDispatchException@8 ; RtlDispatchException(x,x)
.text:004335B3                 jmp     loc_44B0B8

判断返回值是否为1,也就是是否成功处理了异常

1
2
3
.text:0044B0B8 loc_44B0B8:                             ; CODE XREF: KiDispatchException(x,x,x,x,x)+EBBE↑j
.text:0044B0B8                 cmp     al, 1
.text:0044B0BA                 jz      loc_424AC8

如果为1,接下来就会将保存再CONTEXT结构中的数据复制到TrapFrame中,退出函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
.text:00424AC8 loc_424AC8:                             ; CODE XREF: KiDispatchException(x,x,x,x,x)+1C5EB↓j
.text:00424AC8                                         ; KiDispatchException(x,x,x,x,x)+1C743↓j ...
.text:00424AC8                 push    dword ptr [ebp+PreviousMode]
.text:00424ACB                 push    [ebp+Context.ContextFlags]
.text:00424AD1                 lea     eax, [ebp+Context]
.text:00424AD7                 push    eax
.text:00424AD8                 push    [ebp+var_ExceptionFrame]
.text:00424ADE                 push    ebx
.text:00424ADF                 call    _KeContextToKframes@20 ; KeContextToKframes(x,x,x,x,x)
.text:00424AE4
.text:00424AE4 loc_424AE4:                             ; CODE XREF: KiDispatchException(x,x,x,x,x)+1C5FC↓j
.text:00424AE4                                         ; KiDispatchException(x,x,x,x,x)+1C701↓j ...
.text:00424AE4                 mov     ecx, [ebp+var_Cookie]
.text:00424AE7                 call    sub_403063
.text:00424AEC                 call    __SEH_epilog
.text:00424AF1                 retn    14h
.text:00424AF1 _KiDispatchException@20 endp

如果为0,继续判断是否有内核调试器,如果有就继续调用KiDebugRoutine中保存的函数来处理异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.text:0044B0C0 loc_44B0C0:                             ; CODE XREF: KiDispatchException(x,x,x,x,x)+A5↑j
.text:0044B0C0                 mov     eax, ds:_KiDebugRoutine
.text:0044B0C5                 cmp     eax, edi        ; 是否存在内核调试器
.text:0044B0C7                 jz      loc_44B1C2
.text:0044B0CD                 push    1
.text:0044B0CF                 push    edi
.text:0044B0D0                 lea     ecx, [ebp+Context]
.text:0044B0D6                 push    ecx
.text:0044B0D7                 push    esi
.text:0044B0D8                 push    [ebp+var_ExceptionFrame]
.text:0044B0DE                 push    ebx
.text:0044B0DF                 call    eax ; _KiDebugRoutine
.text:0044B0E1                 test    al, al          ; 判断返回值是否为0
.text:0044B0E3                 jz      loc_44B1C2
.text:0044B0E9                 jmp     loc_424AC8

如果此时不存在内核调试器或者KiDebugRoutine中的函数没有处理掉异常,接下来就会产生蓝屏

1
2
3
4
5
6
7
8
.text:0044B1C2 loc_44B1C2:                             ; CODE XREF: KiDispatchException(x,x,x,x,x)+266D2↑j
.text:0044B1C2                                         ; KiDispatchException(x,x,x,x,x)+266EE↑j ...
.text:0044B1C2                 push    edi             ; BugCheckParameter4
.text:0044B1C3                 push    ebx             ; BugCheckParameter3
.text:0044B1C4                 push    dword ptr [esi+0Ch] ; BugCheckParameter2
.text:0044B1C7                 push    dword ptr [esi] ; BugCheckParameter1
.text:0044B1C9                 push    8Eh             ; BugCheckCode
.text:0044B1CE                 call    _KeBugCheckEx@20

如果处理掉了,就会跳转到上面的loc_424AC8来恢复陷阱帧,退出函数。退出函数以后,接下来就会返回到CommonDispatchException执行,该函数会通过iretd返回用户层。

1
2
3
4
.text:00407370 loc_407370:                             ; CODE XREF: Kei386EoiHelper()+14F↓j
.text:00407370                                         ; Kei386EoiHelper()+156↓j
.text:00407370                 add     esp, 4
.text:00407373                 iret

3.用户异常的分发

对于用户异常,同样会首先判断是否是第一次执行,是的话继续判断是否存在内核调试器,如果存在内核调试器且调试端口为0,则会调用KdpTrap函数,如果返回值为1,即成功处理了异常,就会跳转到loc_424AC8退出函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
.text:00440F9E loc_440F9E:                             ; CODE XREF: KiDispatchException(x,x,x,x,x)+9B↑j
.text:00440F9E                 cmp     [ebp+FirstChance], 1    ; 是否是第一次执行
.text:00440FA2                 jnz     loc_44B196
.text:00440FA8                 cmp     ds:_KiDebugRoutine, edi ; 判断是否存在内核调试器
.text:00440FAE                 jz      short loc_440FE6
.text:00440FB0                 mov     eax, large fs:124h
.text:00440FB6                 mov     eax, [eax+_KTHREAD.ApcState.Process]
.text:00440FB9                 cmp     [eax+_EPROCESS.DebugPort], edi ; 调试端口是否为0
.text:00440FBF                 jnz     loc_44B0F3
.text:00440FC5
.text:00440FC5 loc_440FC5:                             ; CODE XREF: KiDispatchException(x,x,x,x,x)+EBC5↑j
.text:00440FC5                 push    edi
.text:00440FC6                 push    dword ptr [ebp+PreviousMode]
.text:00440FC9                 lea     eax, [ebp+Context]
.text:00440FCF                 push    eax
.text:00440FD0                 push    esi
.text:00440FD1                 push    [ebp+var_ExceptionFrame]
.text:00440FD7                 push    ebx
.text:00440FD8                 call    ds:_KiDebugRoutine
.text:00440FDE                 test    al, al          ; 是否处理了异常
.text:00440FE0                 jnz     loc_424AC8

如果不存在内核调试器,或内核调试函数并没有成功处理异常,接下来就会通过调用函数DbgForwardException来将异常发给调试子系统处理,如果成功处理则返回1

1
2
3
4
5
6
7
8
.text:00440FE6 loc_440FE6:                             ; CODE XREF: KiDispatchException(x,x,x,x,x)+EBCB↑j
.text:00440FE6                                         ; KiDispatchException(x,x,x,x,x)+1C5B9↑j
.text:00440FE6                 push    edi
.text:00440FE7                 push    1
.text:00440FE9                 push    esi
.text:00440FEA                 call    _DbgkForwardException@12 ; DbgkForwardException(x,x,x)
.text:00440FEF                 test    al, al
.text:00440FF1                 jnz     loc_424AE4

如果没有处理成功,就会将陷阱帧中的EIP修改为_KiUserExceptionDispatcher,随后退出函数。这样,当程序返回到用户层的时候,就会执行函数_KiUserExceptionDispacher,该函数是在ntdll.dll中

1
2
3
.text:004410EA                 mov     eax, ds:_KiUserExceptionDispatcher
.text:004410EF                 mov     [ebx+68h], eax  ; 为TrapFrame的EIP赋值
.text:004410F6                 jmp     loc_424AE4

在KiUserExceptionDispatcher中会通过调用RltDispatchException来处理异常,如果处理成功,返回值为1,接下来就会调用ZwContinue来修正陷阱帧中的EIP恢复执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
.text:7C92E45C                 public KiUserExceptionDispatcher
.text:7C92E45C KiUserExceptionDispatcher proc near     ; DATA XREF: .text:off_7C923428↑o
.text:7C92E45C
.text:7C92E45C var_14          = dword ptr -14h
.text:7C92E45C var_10          = dword ptr -10h
.text:7C92E45C var_C           = dword ptr -0Ch
.text:7C92E45C var_4           = dword ptr -4
.text:7C92E45C arg_0           = dword ptr  4
.text:7C92E45C
.text:7C92E45C                 mov     ecx, [esp+arg_0]
.text:7C92E460                 mov     ebx, [esp+0]
.text:7C92E463                 push    ecx
.text:7C92E464                 push    ebx
.text:7C92E465                 call    RtlDispatchException
.text:7C92E46A                 or      al, al
.text:7C92E46C                 jz      short loc_7C92E47A
.text:7C92E46E                 pop     ebx
.text:7C92E46F                 pop     ecx
.text:7C92E470                 push    0
.text:7C92E472                 push    ecx
.text:7C92E473                 call    ZwContinue
.text:7C92E478                 jmp     short loc_7C92E485

否则就会调用ZwRaiseException

1
2
3
4
5
6
7
.text:7C92E47A loc_7C92E47A:                           ; CODE XREF: KiUserExceptionDispatcher+10↑j
.text:7C92E47A                 pop     ebx
.text:7C92E47B                 pop     ecx
.text:7C92E47C                 push    0
.text:7C92E47E                 push    ecx
.text:7C92E47F                 push    ebx
.text:7C92E480                 call    ZwRaiseException

ZwRaiseException就会再次进入内核层调用NtRaiseException

1
2
3
4
5
6
7
8
.text:7C92D990                 public ZwRaiseException
.text:7C92D990 ZwRaiseException proc near              ; CODE XREF: KiUserExceptionDispatcher+24↓p
.text:7C92D990                                         ; RtlRaiseException+8E↓p ...
.text:7C92D990                 mov     eax, 0B5h       ; NtRaiseException
.text:7C92D995                 mov     edx, 7FFE0300h
.text:7C92D99A                 call    dword ptr [edx]
.text:7C92D99C                 retn    0Ch
.text:7C92D99C ZwRaiseException endp

此时运行到KiDispatchException先前模式为用户模式清空下,判断是否为第一次运行的时候会跳转到loc_44B196运行,因为此时不是第一次运行

1
2
3
.text:00440F9E loc_440F9E:                             ; CODE XREF: KiDispatchException(x,x,x,x,x)+9B↑j
.text:00440F9E                 cmp     [ebp+FirstChance], 1
.text:00440FA2                 jnz     loc_44B196

在loc_440F9E中,函数会两次调用DbgkForwardException来处理异常,如果处理成功,则返回值为1,然后正常退出函数,如果处理失败,接下来就会结束进程,出现蓝屏

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
.text:0044B196 loc_44B196:                             ; CODE XREF: KiDispatchException(x,x,x,x,x)+1C5AD↑j
.text:0044B196                 push    1
.text:0044B198                 push    1
.text:0044B19A                 push    esi
.text:0044B19B                 call    _DbgkForwardException@12 ; DbgkForwardException(x,x,x)
.text:0044B1A0                 test    al, al
.text:0044B1A2                 jnz     loc_424AE4
.text:0044B1A8                 push    1
.text:0044B1AA                 push    edi
.text:0044B1AB                 push    esi
.text:0044B1AC                 call    _DbgkForwardException@12 ; DbgkForwardException(x,x,x)
.text:0044B1B1                 test    al, al
.text:0044B1B3                 jnz     loc_424AE4
.text:0044B1B9                 push    dword ptr [esi] ; ExitStatus
.text:0044B1BB                 push    0FFFFFFFFh      ; ProcessHandle
.text:0044B1BD                 call    _ZwTerminateProcess@8 ; ZwTerminateProcess(x,x)
.text:0044B1C2
.text:0044B1C2 loc_44B1C2:                             ; CODE XREF: KiDispatchException(x,x,x,x,x)+266D2↑j
.text:0044B1C2                                         ; KiDispatchException(x,x,x,x,x)+266EE↑j ...
.text:0044B1C2                 push    edi             ; BugCheckParameter4
.text:0044B1C3                 push    ebx             ; BugCheckParameter3
.text:0044B1C4                 push    dword ptr [esi+0Ch] ; BugCheckParameter2
.text:0044B1C7                 push    dword ptr [esi] ; BugCheckParameter1
.text:0044B1C9                 push    8Eh             ; BugCheckCode
.text:0044B1CE                 call    _KeBugCheckEx@20

无论是用户层还是内核层都会通过调用RtlDispatcherException来完成异常处理,只不过前者在ntdll.dll中,后者在内核中,两个函数功能和执行逻辑是基本一致的,不过由于“服务”对象不同,它们分别存在于两个模块中

四.异常的处理

1.异常处理机制

从Windows XP系统开始,Windows支持通过以下两种异常处理机制来处理异常。

  • SEH:结构化异常处理机制

  • VEH:向量化异常处理机制

其中SEH既可以在用户模式下和内核模式下都可以使用,而VEH只能在用户模式下使用。在ntdll.dll中和内核中的异常处理函数RtlDispatchException最大的区别也就是ntdll.dll的异常处理函数是处理用户模式下的异常,此时会先调用函数RtlCallVectoredExceptionHandler函数通过VEH来处理异常,如果返回值非0,则处理完成,否则会继续通过SEH处理异常。

 

avatar

2.VEH

A.登记和销毁

VEH的基本思想是通过注册以下原型的回调函数来接收和处理异常。

1
2
3
LONG CALLBACK VectoredHandler(
  __in  PEXCEPTION_POINTERS ExceptionInfo
);

参数ExceptionInfo是指向EXCEPTION_POINTERS结构的指针,该结构包含了指向CONTEXT结构和异常记录结构的指针,定义如下:

1
2
3
4
typedef struct _EXCEPTION_POINTERS { 
    PEXCEPTION_RECORD ExceptionRecord;    // 异常记录   
    PCONTEXT ContextRecord;               // 异常发生时线程上下文
} EXCEPTION_POINTERS, *PEXCEPTION_POINTERS;

通过AddVectoredExceptionHandler可以注册回调函数,如下是函数定义:

1
2
3
4
PVOID WINAPI AddVectoredExceptionHandler(
  __in  ULONG FirstHandler,
  __in  PVECTORED_EXCEPTION_HANDLER VectoredHandler
);
参数 含义
FirstHandler 指定该回调函数的被调用顺序,若为0表示希望最后被调用,若为1希望最先被调用。如果注册了多个回调函数,而且该参数都为非0,那么最后注册的会被最先调用
VectoredHandler 指向要注册的回调函数地址
 

如果注册成功,返回值指向的是系统为该异常处理器分配的结构(VEH_REGISTERATION)指针,应用程序应该保存这个指针,以便销毁向量化异常处理器时使用;如果注册失败,返回值就为0。

 

在AddVectoredExceptionHandler内部,会为每个向量化异常处理器分配一个类型如下结构的长度(长为12字节)。

1
2
3
4
5
6
typedef struct _VEH_REGISTRATION
{
    _VEH_REGISTRATION *next;
    _VEH_REGISTRATION *prev;
    PVECTORED_EXCEPTION_HANDLER pfnVeh;
} _VEH_REGISTRATION, *P_VEH_REGISTRATION;
成员 含义
next 指向下一结构化异常处理器
prev 指向前一结构化异常处理器
pfnVeh 指向该结构化异常处理器的回调函数
 

当有多个向量化异常处理器时,这些向量化异常处理器的VEH_REGISTRATION结构组成一个环状链表。ntdll.dll中的全局变量RtlpCalloutEntryList指向该链表的头。

 

RemoveVectoredExceptionHandler函数用来注销向量化异常处理器,也就是将一个向量化异常处理器从RtlpCalloutEntryList所指向的链表中移除,该函数定义如下:

1
2
3
ULONG WINAPI RemoveVectoredExceptionHandler(
  __in  PVOID Handler
);

其中的参数就是通过AddVectorExceptionHandler注册异常处理器成功时的返回值。

B.向量化异常处理器的执行

前面说过,和内核的异常处理函数相比,ntdll.dll中的异常处理函数会首先通过调用函数RtlCallVectoredExceptionHandler来给向量化异常处理器优先的处理机会。

 

RtlCallVectoredExceptionHandler会直接判断全局链表RtlpCalloutEntryList中是否有向量化异常处理器,如果没有则将al清空作为返回值,也就是返回FALSE

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
.text:7C94A914 ; int __stdcall RtlCallVectoredExceptionHandlers(PEXCEPTION_RECORD pExecptRec, PCONTEXT pContext)
.text:7C94A914 _RtlCallVectoredExceptionHandlers@8 proc near
.text:7C94A914                                         ; CODE XREF: RtlDispatchException(x,x)+14↓p
.text:7C94A914
.text:7C94A914 var_pExceptRec  = dword ptr -8
.text:7C94A914 var_pContext    = dword ptr -4
.text:7C94A914 pExecptRec      = dword ptr  8
.text:7C94A914 pContext        = dword ptr  0Ch
.text:7C94A914
.text:7C94A914 ; FUNCTION CHUNK AT .text:7C9625F9 SIZE 00000054 BYTES
.text:7C94A914
.text:7C94A914                 mov     edi, edi
.text:7C94A916                 push    ebp
.text:7C94A917                 mov     ebp, esp
.text:7C94A919                 push    ecx
.text:7C94A91A                 push    ecx
.text:7C94A91B                 push    edi
.text:7C94A91C                 mov     edi, offset _RtlpCalloutEntryList
.text:7C94A921                 cmp     _RtlpCalloutEntryList.Flink, edi ; 判断是否有成员
.text:7C94A927                 jnz     loc_7C9625F9
.text:7C94A92D                 xor     al, al          ; 清空al
.text:7C94A92F
.text:7C94A92F loc_7C94A92F:                           ; CODE XREF: RtlCallVectoredExceptionHandlers(x,x)+17D2E↓j
.text:7C94A92F                 pop     edi
.text:7C94A930                 leave
.text:7C94A931                 retn    8
.text:7C94A931 _RtlCallVectoredExceptionHandlers@8 endp

如果有的话,会对局部变量赋值,调用RtlEnterCriticalSection函数防止其他线程访问链表

1
2
3
4
5
6
7
8
9
10
11
12
.text:7C9625F9 loc_7C9625F9:                           ; CODE XREF: RtlCallVectoredExceptionHandlers(x,x)+13↑j
.text:7C9625F9                 mov     eax, [ebp+pExecptRec]
.text:7C9625FC                 push    ebx
.text:7C9625FD                 push    esi
.text:7C9625FE                 mov     [ebp+var_pExceptRec], eax
.text:7C962601                 mov     eax, [ebp+pContext]
.text:7C962604                 mov     ebx, offset _RtlpCalloutEntryLock
.text:7C962609                 push    ebx
.text:7C96260A                 mov     [ebp+var_pContext], eax
.text:7C96260D                 call    _RtlEnterCriticalSection@4 ; RtlEnterCriticalSection(x)
.text:7C962612                 mov     esi, _RtlpCalloutEntryList.Flink ; 将链表中下一个成员赋给esi
.text:7C962618                 jmp     short loc_7C96262F

接下来会遍历链表,通过RtlDecodePointer函数来=获得向量化异常处理器的回调函数,然后调用这个回调函数,此时压入会将pExceptRec的地址压入栈中,而紧邻该变量的地址保存的就是参数pContext,所以此时传入的其实是EXCEPTION_POINTERS结构体参数

1
2
3
4
5
6
7
8
9
10
11
12
13
.text:7C96261A loc_7C96261A:                           ; CODE XREF: RtlCallVectoredExceptionHandlers(x,x)+17D1D↓j
.text:7C96261A                 push    [esi+_VEH_REGISTRATION.pfnVeh]
.text:7C96261D                 call    _RtlDecodePointer@4 ; RtlDecodePointer(x)
.text:7C962622                 lea     ecx, [ebp+var_pExceptRec]
.text:7C962625                 push    ecx             ; 参数入栈
.text:7C962626                 call    eax             ; 调用回调函数
.text:7C962628                 cmp     eax, 0FFFFFFFFh ; 返回值是否为-1
.text:7C96262B                 jz      short loc_7C962647
.text:7C96262D                 mov     esi, [esi+_VEH_REGISTRATION.next] ; 指向下一个向量化异常处理器
.text:7C96262F
.text:7C96262F loc_7C96262F:                           ; CODE XREF: RtlCallVectoredExceptionHandlers(x,x)+17D04↑j
.text:7C96262F                 cmp     esi, edi        ; 是否是最后一个链表
.text:7C962631                 jnz     short loc_7C96261A

如果成功处理异常,此时的返回值为-1,就会将局部变量赋值为1,跳转到loc_7C962647处执行代码

1
2
3
.text:7C962647 loc_7C962647:                           ; CODE XREF: RtlCallVectoredExceptionHandlers(x,x)+17D17↑j
.text:7C962647                 mov     byte ptr [ebp+pExecptRec+3], 1
.text:7C96264B                 jmp     short loc_7C962637

如果遍历完链表依然没有成功,就会将局部遍历赋值为0

1
.text:7C962633                 mov     byte ptr [ebp+pExecptRec+3], 0

赋值完以后执行的代码和成功处理异常以后执行的代码是一样的,此时将局部变量保存的结果赋给al作为返回值

1
2
3
4
5
6
7
.text:7C962637 loc_7C962637:                           ; CODE XREF: RtlCallVectoredExceptionHandlers(x,x)+17D37↓j
.text:7C962637                 push    ebx
.text:7C962638                 call    _RtlLeaveCriticalSection@4 ; RtlLeaveCriticalSection(x)
.text:7C96263D                 mov     al, byte ptr [ebp+pExecptRec+3]
.text:7C962640                 pop     esi
.text:7C962641                 pop     ebx
.text:7C962642                 jmp     loc_7C94A92F

最后退出函数

1
2
3
4
5
.text:7C94A92F loc_7C94A92F:                           ; CODE XREF: RtlCallVectoredExceptionHandlers(x,x)+17D2E↓j
.text:7C94A92F                 pop     edi
.text:7C94A930                 leave
.text:7C94A931                 retn    8
.text:7C94A931 _RtlCallVectoredExceptionHandlers@8 endp

此时如果任何一个向量化异常处理器处理掉异常,那么返回值都会为1。如果都没处理,返回值为0,接下来就要通过结构化异常来处理异常。

3.结构化异常处理

A.SEH介绍

为了让系统和应用程序代码都可以简单方便地支持异常处理,Windows系统定义了一套标准的机制来规范异常处理代码的设计(对程序员)和编译(对编译器),这套机制称为结构化异常处理。

 

从系统(广义)的角度看,SEH是Windows操作系统中的异常分发和处理机制的总称,其实现遍布在Windows系统的很多模块和数据结构中。

 

从编程(狭义)的角度看,SEH是一套规范,利用这套规范,程序员可以编写处理代码来复用系统的异常处理设施。可以将其理解为是操作系统的异常机制的对外接口,也就是如何在Windows程序中使用Windows系统的异常处理机制。

B.结构化异常处理器的保存

在用户模式下,fs:[0]寄存器指向的是线程环境块(TEB),TEB的起始处有一个称为线程信息块的结构(TIB)。而在内核模式下,fs:[0]指向的则是KPCR结构的开始处,其第一个成员也是线程信息块结构(TIB)。TIB结构包含了异常,线程栈等信息,该结构定义如下:

1
2
3
4
5
6
7
8
9
10
kd> dt _NT_TIB
ntdll!_NT_TIB
   +0x000 ExceptionList    : Ptr32 _EXCEPTION_REGISTRATION_RECORD
   +0x004 StackBase        : Ptr32 Void
   +0x008 StackLimit       : Ptr32 Void
   +0x00c SubSystemTib     : Ptr32 Void
   +0x010 FiberData        : Ptr32 Void
   +0x010 Version          : Uint4B
   +0x014 ArbitraryUserPointer : Ptr32 Void
   +0x018 Self             : Ptr32 _NT_TIB
成员 作用
ExceptionList 指向当前线程的异常链表(SEH)
StackBase 栈的基地址
StackLimit 栈的边界
 

TIB起始处保存的是ExceptionList字段,所以fs:[0]的内容就是ExceptionList字段的内容,该字段是_EXCEPTION_REGISTRATION_RECORD结构体,定义如下:

1
2
3
4
kd> dt _EXCEPTION_REGISTRATION_RECORD
nt!_EXCEPTION_REGISTRATION_RECORD
   +0x000 Next             : Ptr32 _EXCEPTION_REGISTRATION_RECORD
   +0x004 Handler          : Ptr32 _EXCEPTION_DISPOSITION
成员 作用
Next 指向下一个_EXCEPTION_REGISTRATION_RECORD结构
Handler 指向这个异常处理器的处理函数
 

根据定义可以知道,所有异常处理函数通过_EXCEPTION_REGISTRATION_RECORD结构保存在栈中。通过Next字段可以找到其下一个异常处理函数,当Next为0xFFFFFFFF(-1)的时候,代表没有下一个异常处理函数,Handler则指向了异常处理函数,指向的函数原型如下:

1
2
3
4
EXCEPTION_DISPOSITION Handler(_EXCEPTION_RECORD *ExceptionRecord,
                              void *EstablisherFrame,
                              _CONTEXT *ContextRecord,
                              void *DispatcherContext);
含义
ExceptionRecord 指向EXCEPTION_RECORD结构,用来描述要处理的异常
EstablisherFrame 指向的是放在栈帧中的异常登记结构
ContextRecord 用来传递发生异常时的线程上下文结构
DispatcherContext 供异常分发函数来传递额外信息
 

可以得出结论,系统可以通过fs:[0]这种便捷方式引用ExceptionList字段,获得一个链表的头指针,这个链表的每个节点是一个_EXCEPTION_REGISTRATION_RECORD结构,描述了一个结构化的异常处理器(handler),该链表通过成员Next连接起来。当有异常发生时,系统就可以通过Next成员,依次调用这个链表上的处理器来分发异常。

可以把结构化依次处理看作操作系统于用户代码协同处理软硬件依次的一种模型,而fs:[0]链条便是这二者的接口。当有异常需要处理时,操作系统通过fs:[0]链条来寻找异常处理器,给用户代码处理异常清空的机会

C.结构化异常处理器的登记与销毁

由上内容可知道,结构化异常处理器是要保存在栈上的。因此,可以用以下代码来登记一个结构化异常处理器:

1
2
3
push seh_handler    // 处理器的地址
push fs:[0]         // 前一个结构化异常处理器的地址
mov fs:[0], esp     // 登记新的结构

其中第一行是将一个具有SEH标准的函数原型的处理函数地址,第二行将fs:[0],也就是字段ExceptionList的当前值入栈,栈上就建立了一个EXCEPTION_REGISTRATION_RECORD结构,栈顶地址便是这个结构的地址,因此,第3行把esp寄存器的内容赋给fs:[0],实质上就是把刚刚注册的异常处理器的地址赋给ExceptionList。当CPU继续指向这段代码的下面代码时候,如果有异常发生,那么系统在遍历fs:[0]链条时便会首先找到这个异常处理器,给其处理器机会。因此,以上代码片段一旦插入,那么它之后的代码便进入了它所安装的异常处理器的“保护”范围。

 

可以使用如下代码来注销前面登记的异常处理器:

1
2
3
mov eax, [esp]     // 从栈顶取得前一个异常登记结构的地址
mov fs:[0], eax    // 将前一个异常结构的地址赋给fs:[0]
add esp, 8         // 清理栈上的异常登记结构

此时esp指向的是刚注册的结构化异常处理器的EXCEPTION_REGISTRATION_RECORD结构体的地址,其第一个成员Next指向的是前一个异常登记结构的地址,将其赋给fs:[0],也就是赋给ExceptionList,这样,fs:[0]的内容就恢复到了安装这个结构化异常处理器前的状态。

D.结构化异常处理器的执行

对于ntdll.dll中的RtlDispatcherException,如果VEH没有成功处理异常,接着就会通过SEH来处理异常。

 

通过调用RtlpGetStackLimits来获取栈基址和栈界限

1
2
3
4
5
.text:7C94A972                 lea     eax, [ebp+var_StackBase]
.text:7C94A975                 push    eax
.text:7C94A976                 lea     eax, [ebp+var_StackLimit]
.text:7C94A979                 push    eax
.text:7C94A97A                 call    _RtlpGetStackLimits@8

RtlpGetStackLimits函数实现如下(根据Tib结构可以知道,fs:[0x8]为栈边界,fs:[0x4]为栈基址):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.text:7C9233DC ; __stdcall RtlpGetStackLimits(x, x)
.text:7C9233DC _RtlpGetStackLimits@8 proc near         ; CODE XREF: RtlDispatchException(x,x)+2A↓p
.text:7C9233DC                                         ; RtlUnwind(x,x,x,x)+26↓p
.text:7C9233DC
.text:7C9233DC arg_StackLimit  = dword ptr  4
.text:7C9233DC arg_StackBase   = dword ptr  8
.text:7C9233DC
.text:7C9233DC                 mov     eax, large fs:8
.text:7C9233E2                 mov     ecx, [esp+arg_StackLimit]
.text:7C9233E6                 mov     [ecx], eax
.text:7C9233E8                 mov     eax, large fs:4
.text:7C9233EE                 mov     ecx, [esp+arg_StackBase]
.text:7C9233F2                 mov     [ecx], eax
.text:7C9233F4                 retn    8
.text:7C9233F4 _RtlpGetStackLimits@8 endp

调用RtlpGetRegistrationHead来获取ExceptionList字段

1
.text:7C94A97F                 call    _RtlpGetRegistrationHead@0 ; RtlpGetRegistrationHead()

根据其函数实现可以知道,是将ExceptionList赋给eax

1
2
3
4
5
6
.text:7C9233F8 ; _DWORD __stdcall RtlpGetRegistrationHead()
.text:7C9233F8 _RtlpGetRegistrationHead@0 proc near    ; CODE XREF: RtlDispatchException(x,x)+2F↓p
.text:7C9233F8                                         ; RtlUnwind(x,x,x,x)+90↓p
.text:7C9233F8                 mov     eax, large fs:0
.text:7C9233FE                 retn
.text:7C9233FE _RtlpGetRegistrationHead@0 endp

判断获取的ExceptionList的Next成员是否为-1

1
2
3
.text:7C94A988                 mov     ebx, eax        ; 将ExceptionList赋给ebx
.text:7C94A98A                 cmp     ebx, 0FFFFFFFFh
.text:7C94A98D                 jz      loc_7C94AA22

如果为-1,就会退出函数,此时局部变量var_res为0,将其赋给eax,就是指定返回值为0

1
2
3
4
5
6
7
8
9
10
11
12
13
.text:7C94AA21 loc_7C94AA21:                           ; CODE XREF: RtlDispatchException(x,x)-650↑j
.text:7C94AA21                                         ; RtlDispatchException(x,x)-63F↑j ...
.text:7C94AA21                 pop     edi
.text:7C94AA22
.text:7C94AA22 loc_7C94AA22:                           ; CODE XREF: RtlDispatchException(x,x)+3D↑j
.text:7C94AA22                 pop     ebx
.text:7C94AA23
.text:7C94AA23 loc_7C94AA23:                           ; CODE XREF: RtlDispatchException(x,x)+2398E↓j
.text:7C94AA23                 mov     al, [ebp+var_res]
.text:7C94AA26                 pop     esi
.text:7C94AA27                 leave
.text:7C94AA28                 retn    8
.text:7C94AA28 _RtlDispatchException@8 endp

判断ExceptionList是否在栈空间内以及其最低两位是否为1

1
2
3
4
5
6
7
8
.text:7C94A994 loc_7C94A994:                           ; CODE XREF: RtlDispatchException(x,x)-645↑j
.text:7C94A994                 cmp     ebx, [ebp+var_StackLimit]
.text:7C94A997                 jb      loc_7C94A316    ; 小于则跳转
.text:7C94A99D                 lea     eax, [ebx+8]    ; 取ExceptionList成员地址+8的地址
.text:7C94A9A0                 cmp     eax, [ebp+var_StackBase]
.text:7C94A9A3                 ja      loc_7C94A316    ; 高于则跳转
.text:7C94A9A9                 test    bl, 3
.text:7C94A9AC                 jnz     loc_7C94A316

如果任何一个条件不满足,接下来就会为ExceptionFlags赋值以后退出函数

1
2
3
4
.text:7C94A316 loc_7C94A316:                           ; CODE XREF: RtlDispatchException(x,x)+47↓j
.text:7C94A316                                         ; RtlDispatchException(x,x)+53↓j ...
.text:7C94A316                 or      [esi+_EXCEPTION_RECORD.ExceptionFlags], 8
.text:7C94A31A                 jmp     loc_7C94AA21

如果都满足条件,接下来就会判断异常处理函数地址是否不在栈内,如果在栈内就会跳转到上面的loc_7C94A316退出函数,该机制就是为了支持DEP功能的

1
2
3
4
5
.text:7C94A9B2                 mov     eax, [ebx+_EXCEPTION_REGISTRATION_RECORD.Handler]
.text:7C94A9B5                 cmp     eax, [ebp+var_StackLimit]
.text:7C94A9B8                 jb      short loc_7C94A9C3 ; 小于则跳转
.text:7C94A9BA                 cmp     eax, [ebp+var_StackBase]
.text:7C94A9BD                 jb      loc_7C94A316

如果不在栈中,通过RtlIsValidHandler来判断异常处理函数是否合法,不合法的话依然会跳转到上面的loc_7C94A316退出函数,该机制是为了支持SAFESEH

1
2
3
4
5
.text:7C94A9C3 loc_7C94A9C3:                           ; CODE XREF: RtlDispatchException(x,x)+68↑j
.text:7C94A9C3                 push    eax
.text:7C94A9C4                 call    _RtlIsValidHandler@4 ; RtlIsValidHandler(x)
.text:7C94A9C9                 test    al, al
.text:7C94A9CB                 jz      loc_7C94A316

通过验证以后就会通过RtlpExecuteHandlerForException来处理异常,并将返回值赋给edi

1
2
3
4
5
6
7
8
9
.text:7C94A9DE loc_7C94A9DE:                           ; CODE XREF: RtlDispatchException(x,x)+239A4↓j
.text:7C94A9DE                 push    [ebx+_EXCEPTION_REGISTRATION_RECORD.Handler]
.text:7C94A9E1                 lea     eax, [ebp+var_pNestedRegistration]
.text:7C94A9E4                 push    eax
.text:7C94A9E5                 push    [ebp+pContext]
.text:7C94A9E8                 push    ebx
.text:7C94A9E9                 push    esi
.text:7C94A9EA                 call    _RtlpExecuteHandlerForException@20 ; RtlpExecuteHandlerForException(x,x,x,x,x)
.text:7C94A9F6                 mov     edi, eax        ; 将返回值赋给edi

该函数返回值会是一个枚举类型EXCEPTION_DISPOSITION的常量,含义如下:

常量 取值 含义
ExceptionContinueExecution 0 恢复执行触发异常的代码
ExceptionContinueSearch 1 继续寻找下一个异常处理器
ExceptionNestedException 2 在调用Handler函数过程中又发生了异常,即嵌套一层
 

接下来就是根据返回值进行操作,首先判断是否为0,且ExceptionFlags是否满足条件,如果满足条件就会将局部变量var_res赋值为1,接下来就会继续执行上面的loc_4C994AA21的代码,将var_res的值赋给eax作为局部变量返回,代表处理成功,然后退出函数

1
2
3
4
5
6
7
8
.text:7C94AA07 loc_7C94AA07:                           ; CODE XREF: RtlDispatchException(x,x)+239BF↓j
.text:7C94AA07                 mov     eax, edi
.text:7C94AA09                 xor     ecx, ecx        ; ecx清0
.text:7C94AA0B                 sub     eax, ecx
.text:7C94AA0D                 jnz     loc_7C94A2F5
.text:7C94AA13                 test    byte ptr [esi+_EXCEPTION_RECORD.ExceptionFlags], EXCEPTION_NONCONTINUABLE
.text:7C94AA17                 jnz     loc_7C96E351
.text:7C94AA1D                 mov     [ebp+var_res], 1

如果ExceptionFlags不满足条件,会调用RtlRaiseException分发异常

1
2
3
4
5
6
7
8
.text:7C96E351 loc_7C96E351:                           ; CODE XREF: RtlDispatchException(x,x)+C7↑j
.text:7C96E351                 lea     eax, [ebp+var_ExecptRec]
.text:7C96E354                 push    eax
.text:7C96E355                 mov     [ebp+var_ExecptRec.ExceptionCode], EXCEPTION_NONCONTINUABLE_EXCEPTION
.text:7C96E35C                 mov     [ebp+var_ExecptRec.ExceptionFlags], 1
.text:7C96E363                 mov     [ebp+var_ExecptRec.ExceptionRecord], esi
.text:7C96E366                 mov     [ebp+var_ExecptRec.NumberParameters], ecx
.text:7C96E369                 call    _RtlRaiseException@4

如果返回值为1,且ExceptionFlags符合条件,就会跳转到上面的loc_7C94AA21处退出函数

1
2
3
4
5
6
7
.text:7C94A2F5 loc_7C94A2F5:                           ; CODE XREF: RtlDispatchException(x,x)+BD↓j
.text:7C94A2F5                 dec     eax
.text:7C94A2F6                 jnz     loc_7C96E314
.text:7C94A2FC
.text:7C94A2FC loc_7C94A2FC:                           ; CODE XREF: .text:7C96E36E↓j
.text:7C94A2FC                 test    byte ptr [esi+_EXCEPTION_RECORD.ExceptionFlags], 8
.text:7C94A300                 jnz     loc_7C94AA21

否则就取出下一异常处理器,判断Next是否为-1,不为-1就会跳转到loc_7C94A994继续上面操作,为-1则退出函数

1
2
3
4
5
6
.text:7C94A306 loc_7C94A306:                           ; CODE XREF: .text:7C96E334↓j
.text:7C94A306                                         ; RtlDispatchException(x,x)+239F3↓j ...
.text:7C94A306                 mov     ebx, [ebx+_EXCEPTION_REGISTRATION_RECORD.Next]
.text:7C94A308                 cmp     ebx, 0FFFFFFFFh
.text:7C94A30B                 jnz     loc_7C94A994
.text:7C94A311                 jmp     loc_7C94AA21

如果返回值不为0, 1, 2就会调用RtlRaiseException来分发异常

1
2
3
4
5
6
7
8
9
10
.text:7C96E314 loc_7C96E314:                           ; CODE XREF: RtlDispatchException(x,x)-65A↑j
.text:7C96E314                 dec     eax
.text:7C96E315                 jz      short loc_7C96E339
.text:7C96E317                 lea     eax, [ebp+var_ExecptRec]
.text:7C96E31A                 push    eax
.text:7C96E31B                 mov     [ebp+var_ExecptRec.ExceptionCode], EXCEPTION_INVALID_DISPOSITION
.text:7C96E322                 mov     [ebp+var_ExecptRec.ExceptionFlags], 1
.text:7C96E329                 mov     [ebp+var_ExecptRec.ExceptionRecord], esi
.text:7C96E32C                 mov     [ebp+var_ExecptRec.NumberParameters], ecx
.text:7C96E32F                 call    _RtlRaiseException@4

如果为2,对pNestedRegistration继续判断,如果地址小于pExecptRec,来决定是否要为pExecptRec赋值,随后跳转到上面的loc_7C94A306来取下一个异常处理器后继续执行上述循环过程

1
2
3
4
5
6
7
.text:7C96E339 loc_7C96E339:                           ; CODE XREF: RtlDispatchException(x,x)+239C5↑j
.text:7C96E339                 mov     eax, [ebp+var_pNestedRegistration]
.text:7C96E33C                 or      [esi+_EXCEPTION_RECORD.ExceptionFlags], 10h
.text:7C96E340                 cmp     eax, [ebp+pExecptRec]
.text:7C96E343                 jbe     loc_7C94A306    ; 低于等于则跳转
.text:7C96E349                 mov     [ebp+pExecptRec], eax
.text:7C96E34C                 jmp     loc_7C94A306

下图是对上述过程的展现:

 

avatar

 

对于异常处理函数RtlpExecuteHanlderForException,首先将函数地址SehHandler赋给edx,随后跳转到ExecuteHandler

1
2
3
4
5
6
.text:7C923247 ; __stdcall RtlpExecuteHandlerForException(x, x, x, x, x)
.text:7C923247 _RtlpExecuteHandlerForException@20 proc near
.text:7C923247                                         ; CODE XREF: RtlDispatchException(x,x)+9A↓p
.text:7C923247                 mov     edx, offset SehHandler
.text:7C92324C                 jmp     short ExecuteHandler@20 ; ExecuteHandler(x,x,x,x,x)
.text:7C92324C _RtlpExecuteHandlerForException@20 endp

而ExecuteHandler则是通过ExecuteHandler2实现的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
.text:7C923256 ; int __stdcall ExecuteHandler(PVOID pExceptionRecord, PVOID pExcepttionRegistration, PCONTEXT pContext, int *pDispatcherContext, PVOID pfnHandler)
.text:7C923256 ExecuteHandler@20 proc near             ; CODE XREF: RtlpExecuteHandlerForException(x,x,x,x,x)+5↑j
.text:7C923256
.text:7C923256 pExceptionRecord= dword ptr  4
.text:7C923256 pExcepttionRegistration= dword ptr  8
.text:7C923256 pContext        = dword ptr  0Ch
.text:7C923256 pDispatcherContext= dword ptr  10h
.text:7C923256 pfnHandler      = dword ptr  14h
.text:7C923256
.text:7C923256                 push    ebx
.text:7C923257                 push    esi
.text:7C923258                 push    edi
.text:7C923259                 xor     eax, eax
.text:7C92325B                 xor     ebx, ebx
.text:7C92325D                 xor     esi, esi
.text:7C92325F                 xor     edi, edi
.text:7C923261                 push    [esp+pfnHandler] ; pfnHandler
.text:7C923265                 push    [esp+pDispatcherContext] ; pDispatcherContext
.text:7C923269                 push    [esp+pContext] ; pContext
.text:7C92326D                 push    [esp+pExcepttionRegistration] ; pExcepttionRegistration
.text:7C923271                 push    [esp+pExceptionRecord] ; pExceptionRecord
.text:7C923275                 call    ExecuteHandler2@20 ; ExecuteHandler2(x,x,x,x,x)
.text:7C92327A                 pop     edi
.text:7C92327B                 pop     esi
.text:7C92327C                 pop     ebx
.text:7C92327D                 retn    14h
.text:7C92327D ExecuteHandler@20 endp

ExecuteHandler2则是会将SehHandler函数登记到SEH异常链条中,随后在调用传入的函数,此时的edx在前面被赋值为SehHandler函数地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
.text:7C923282 ; int __stdcall ExecuteHandler2(PVOID pExceptionRecord, PVOID pExcepttionRegistration, PCONTEXT pContext, PVOID pDispatcherContext, PVOID pfnHandler)
.text:7C923282 ExecuteHandler2@20 proc near            ; CODE XREF: ExecuteHandler(x,x,x,x,x)+1F↑p
.text:7C923282
.text:7C923282 pExceptionRecord= dword ptr  8
.text:7C923282 pExcepttionRegistration= dword ptr  0Ch
.text:7C923282 pContext        = dword ptr  10h
.text:7C923282 pDispatcherContext= dword ptr  14h
.text:7C923282 pfnHandler      = dword ptr  18h
.text:7C923282
.text:7C923282                 push    ebp
.text:7C923283                 mov     ebp, esp
.text:7C923285                 push    [ebp+pExcepttionRegistration]
.text:7C923288                 push    edx
.text:7C923289                 push    large dword ptr fs:0
.text:7C923290                 mov     large fs:0, esp ; 注册SEH异常处理器
.text:7C923297                 push    [ebp+pDispatcherContext]
.text:7C92329A                 push    [ebp+pContext]
.text:7C92329D                 push    [ebp+pExcepttionRegistration]
.text:7C9232A0                 push    [ebp+pExceptionRecord]
.text:7C9232A3                 mov     ecx, [ebp+pfnHandler]
.text:7C9232A6                 call    ecx             ; 调用函数
.text:7C9232A8                 mov     esp, large fs:0 ; 销毁SEH异常处理器
.text:7C9232AF                 pop     large dword ptr fs:0
.text:7C9232B6                 mov     esp, ebp
.text:7C9232B8                 pop     ebp
.text:7C9232B9                 retn    14h
.text:7C9232B9 ExecuteHandler2@20 endp

异常处理的一个特征就是线程相关性。也就是说,异常的分发和处理是在线程范围内进行的,异常处理器的注册也是相对线程而言

4.VEH和SEH的区别和联系

  • 从应用范围来:SEH既可以在用户态(应用程序)代码中,也可也在内核态(比如驱动程序)代码中,但VEH只能在用户态代码中。另外,VEH只有在XP或更高版本的Windows系统中才能使用。

  • 从优先级的角度:对于同时注册了VEH和SEH的代码所触发的异常,VEH比SEH先得到处理权

  • 从注册方式角度看:SEH的注册信息是以固定的结构存储在线程栈中,不同层次的各个SEH的注册信息依次被压入栈中,分布栈的不同位置,依靠结构内的指针和联系,因为人们经常将一个函数所对应的区域称为栈帧,所以结构化异常处理器又经常称为基于帧的异常处理器;VEH的注册信息是存储在进程的内存堆中

  • 从作用域看:VEH处理器相对于整个进程都有效,具有全局性;结构化异常处理器是动态建立在所在函数的栈帧上,会随着函数的返回而注销,因此SEH只对当前函数所这个函数所调用的子函数有效

  • 从编译角度看:SEH的注册和销毁都依靠编译器编译时所生成的数据结构与代码的,VEH的注册和销毁都是通过系统的API显示完成的,不需要编译器特殊处理

下篇:Windows内核学习笔记之异常(下)


【公告】 讲师招募 | 全新“预付费”模式,不想来试试吗?

最后于 2021-12-31 21:27 被1900编辑 ,原因:
收藏
点赞1
打赏
分享
最新回复 (0)
游客
登录 | 注册 方可回帖
返回