-
-
[原创]Windows内核学习笔记之异常(上)
-
2021-12-30 12:14 24926
-
一.中断和异常
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对应的用于异常的状态码:
2.登记CPU异常
对于产生的CPU异常,处理器会根据IDT表查询到需要执行的函数,如下所示是部分产生了CPU异常以后会执行的处理例程:
以除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函数来实现的
而_CxxThrowException函数则会调用kernel32.dll中的RaiseException,此时的传入的第一个参数在RaiseException中会赋给ExceptionCode,此时是0xE06D7363,该值取决于编译器,每次调用的时候都是这个数值
在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中,以供向调试器和异常处理器函数报告异常时使用,接下来根据先前模式来选择操作流程
根据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处理异常。
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 |
下图是对上述过程的展现:
对于异常处理函数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显示完成的,不需要编译器特殊处理
阿里云助力开发者!2核2G 3M带宽不限流量!6.18限时价,开 发者可享99元/年,续费同价!