《加密与解密》-系统篇-WIndows下的异常处理

目录:

异常处理的基本概念

SEH的概念及基本知识

SEH异常处理程序原理与设计

向量化异常处理

x64平台上的异常处理

异常处理的实际应用

 

笔记

 

 

 

 

 

 

 

 

 

 

1.异常处理基本概念

lntel公司在从386开始的IA-32家族处理器中引人了中断(lnterrupt)和异常(Exception〕
概念。中断是由外部硬件设备或异步事件产生的,而异常是由内部事件产生的,又可分为故障、陷
阱和终止3类。故障和陷阱,正如其名称所暗示的,是可恢复的;终止类异常是不可恢复的,如果发生了这种异常,系统必须重启。

1.1异常列表

所谓异常就是在应用程序正褙执行过程中发生的不正常事件。由CPU引发的异常称为硬件异
,例如访问一个无效的内存地址。由操作系统或应用程序引发的异常称为软件异常

中断类型号  类型                            相关指令              

1      除数为0时中断    DIV,IDIV
2      调试异常      任何指令

3      断点中断      INT3指令
4      溢出中断      INTO
5      边界检查      BOUND
6      非法指令故障    非法指令编码或操作数
7      设备不可用     浮点指令或WAIT 
8      双重故障      任何指令
a      无效TSS中断    JMP,CALL,IRET,中断
b      段不存在异常    装载段寄存器    
c      栈异常        装载SS寄存器或SS段寻址
d       通用保护异常    任何特权指令
e      页异常       任何访问存储器的指令

 

了CPU能够捕获一个事件并引发一个硬件异常外,在代码中可以主动引发一个软件异常,这
只需调用RaiseException()函数,示例如下。

1.2 异常处理的基本过程

Windows正常启动后,将运行在保护模式下,当有中断或异常发生时CPU会通过中断描述符
表(DescriptorTable,IDT)来寻找处理函数。因此,IDT表是CPU(硬件)和操作系统(软
件)交接中断和异常的关口

1.IDT
IDT是一张位于物理内存中的线性表,共有256项。在32位模式下每个IDT项的长度是8字节
64位模式下则为64字节操作系统在启动阶段会初始化这个表,系统中的每个CPU都有一份
IDT的拷贝。下面主要讨论32位模式下的IDT.
IDT的位置和长度是由CPU的IDTR寄存器描述的。IDTR寄存器共有48位,其中高32位是表
的基址,低16位是表的长度。尽管可以使用SIDT和LIDT指令来读写该寄存器但LIDT是特权指
令,只能在Ring0特权级下运行。
IDT的每一项都是一个门结构,它是发生中断或异常时CPU转移控制权的必经之路,包括如下
3种门描述符。
*任务门(Task-gate)描述符,主要用于CPU的任务切换(TSS功能)。
*中断门(lntenupt-gate)描述符,主要用于描述中断处理程序的人口
*陷阱门(Trap-gate)描述符,主要用于描述异常处理程序的人口
使用WinDbg的本地内核调试模式可以比较方便地观察IDT(WindowsXPSP3),示例如下。

可以看到,02、08和12项就是任务门的处理过程,其他项是陷阱门的处理过程,在一些没有
显示的内容中包含了中断门的处理过程。

2.异常处理的准备工作:

当有中断或异常发生时CPU会根据中断类型号(这里其实把异常也视为一种中断)转而执行
对应的中断处理程序对异常来说就是上面看到的KiTrapXX函数。例如,中断号03对应于一个断
点异常,当该异常发生时,CPU就会执行nt!KiTrap03函数来处理该异常。各个异常处理函数除了针
对本异常的特定处理之外。通常会将异常信息进行封装,以便进行后续处理。
封装的内容主要有两部分。一部分是异常记录,包含本次异常的信息,该结构定义如下。

typedef  struct  _EXCEPTION_RECORD{
  NTSTATUS  ExceptionCode;//异常代码
  ULONG  ExceptionFlags;///异常标志
  struct  EXCEPTION_RECORD  *ExceptionRecord;//指向另一个EXCEPTION_RECORD的指针
  PVOID  ExceptionAddress;//异常发生的地址
  ULONG  NumberParameters;//下面的Exception1nformation含有的元素数目
  ULONG_PTR  ExceptionInformation[EXCEPTION_MÄXIMUM_PARAMETERS];//附加信息
}EXCEPTION_RECORD;

 

其中,ExceptionCode字段定义了异常的产生原因,表8.2中列出了一些常见的异常产生原因。
当然,也可以定义自己的ExcEptionCode,自定义代码可在API函数RaiseException中使用。

另一部分被封装的内容称为陷阱帧,它精确描述了发生异常时线程的状态(Windows的任务调
度是矿线程的) 该结构与处理器高度相关,因此在不同的平台上(lntelx86/x64、Alpha
和处理器等)有不同的定义。在常见的x86平台上,该结构定义如下

typedef struct _KTRAP_FRAME{

.............................................

}

可以看到,上述结构中包含每个寄存器的状态,但该结构一般仅供系统内核自身或者调试系统
使用。当需要把控制权交给用户注册的异常处理程序时,会将上述结构转换成一个名为CONTEXT
的结构,它包含线程运行时处理器各主要寄存器的完整镜像,用于保存线程运行环境。
x86平台上的CONTEXT结构如下。

typedef struct _CONTEXT{
//标志位.表示整个结构中哪些部分是有效的
ULONG ContextFlags;
//当ContextFlags包含CONTEXT_DEBUG_REGISTERS时
,以下部分有效
ULONG  Dr0;
ULONG  Dr1;
ULONG  Dr2;
ULONG  Dr3;
ULONG  Dr6;
ULONG  Dr7;

//当contextFlags包含CONTEXT_FLOATING_POINT时,以下部分有效
FLOATING_SAVE_AREA   FIoatSave;

//当包含CONTEXTSEGMENTS时,以下部分有效
ULONG  SegGs
ULONG  SegFs;
ULONG  SegEs;
ULONG  SegDs
//当ContextFlags包含.CONTEXT_INTEGER时以下部分有效
ULONG  Edi;
ULONG  Esi;
ULONG  Ebx;
ULONG  Edx;
ULONG  Ecx;
ULONG  Eax;
//当ContextFIags包含CONTZXTCONTROL时.以下部分有效
ULONG  Ebp;
ULONG  Eip;
ULONG  SegCs:
ULONG  EPIags:
ULONG  Esp;
ULONG  SegSs;

//当ContextFlags包含CONTEXT_EXTENDED_REGISTERS时·以下部分有效
UCHAR  ExtendedRegisters[MAXIMUMSUPPORTEDZXTENSION];
) CONTEXT;

该结构的大部分域是不言自明的。需要解释的是,其第1个域ContextFlags表示该结构中的哪
些域有效,当需要用CONTEXT结构保存的信息恢复执行时可对应更新,这为有选择地更新部分域而非全部域提供了有效的手段。

包装完毕,异常处理函数会进一步调用系统内核的nt!KiDispatchException函数来处理异常。因
此,只有深人分析KiDispatchException函数的执行过程,才能理解异常是如何被处理的。该函数原
型及各参数的含义如下,其第1个和第3个参数正是上面封装的两个结构

TODO

 

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

3.内核态的异常处理过程
当PreviousMode为时,表示是内核模式下产生的异常,此时KiDispatchException会
按以下步骤分发异常。
(1) 检测当前系统是否正在被内核调试器调试。如果内核调试器不存在.就跳过本步骤。如果
内核调试器存在,系统就会把异常处理的控制权转交给内核调试器,并注明是第1次处理机会
(FirstChance)。内核调试器取得控制权之后,会根据用户对异常处理的设置来确定是否要处理该异
常。如果无法确定该异常是否需要处理,就会发生中断,把控制权交给用户,由用户决定是否处理。
如果调试器正确处理了该异常.那么发生异常的线程就会回到原来产生异常的位置继续执行。
(2) 如果不存在内核调试器,或者在第1次处理机会出现时调试器选择不处理该异常,系统就会
调用nt!RtlDispatchException函数根据线程注册的结构化异常处理(StmcturedExceptionHandling.
SEH,其细节会在8.2节讨论)过程来处理该异常。
(3) 如果nt!RtlDispatchException函数没有处理该异常,系统会给调试器第2次处理机会(second 
Chance),此时调试器可以再次取得对异常的处理权。
(4) 如果不存在内核调试器,或者在第2次机会调试器仍不处理,统就认为在这种情况下不
能继续运行了。为了避免引起更加严重的、不可预知的错误,系统会直调用KeBugCheckEx产生
一个错误码为"KERNEL-MODE—EXCEPCION—NOT—HANDLED”(其值为0x00佣008E)的(俗
称蓝屏错误)。
可以看到,在上述异常处理过程中,只有在某步骤中异常未得到理,才会进行下一处理过
程。在任何时候,只要异常被处理了,就会终止整个异常处理过程。

4,用户态的异常处理过程

当PreviousMode为UserMode时,表示是用户模式下产生的异常。此时KiDispatchException函

数仍然会检测内核调试器是否存在。如果内核调试器存在,会优先把控制权交给内核调试器进行处

理。所以,使用内核调试器调试用户态程序是完全可行的,并且不依进程的调试端口。在大多

数情况下,内核调试器对用户态的异常不感兴趣,也就不会去处理它,此时nt!KIDispatchException

函数仍然像处理内核态异常一样按两次处理机会进行分发,主要过程如下。

(1) 如果发生异常的程序正在被调试,那么将异常信息发送给正在调试它的用户态调试器,给

调试器第1次处理机会;如果没有被调试,跳过本步。

(2) 如果不存在用户态调试器或调试器未处理该异常,那么在栈上放置EXCEPRION_RECORD和

CONTEXT两个结构并将控制权返回用户态ntdll.dll中的KiUserExceptiohDispatcher函数由它调用

ntdll!RtlDispatchException函数进行用户态的异常处理这一部分涉及SEH和VEH两种异常处理机

。其中,SEH部分包括应用程序调用API函数SetUnhandledExceptionilter设置的顶级异常处理,

但如果有调试器存在,顶级异常处理会被跳过,进人下一阶段的处理,否则将由顶级异常处理程序

进行终结处理(通常是显示一个应用程序错误对话框并根据用户的选择定是终止程序还是附加到

调试器)。如果没有调试器能附加于其上或调试器还是处理不了异常,系统就调用ExitProcess函数

来终结程序。

(3) 如果ntdll!RtlDispatchException函数在调用用户态的异常处理过程中未能处理该异常,那么

异常处理过程会再次返回nt!KiDispatchException它将再次把异常信息送给用户态的调试器,给

调试器第2次处理机会。如果没有调试器存在,则不会进行第2次分发,而是直接结束进程。

(4) 如果第2次机会调试器仍不处理,nt!KiDispatchException会再次尝试把异常分发给进程的异

常端口进行处理。该端口通常由子系统进程进行监听。子系统监听到该错误后,通常会显

示一个“应用程序错误”对话框,如图&1所示,用户可以单击“确定”按钮或者最后将其附加到

调试器上的“取消”按钮。如果没有调试器能附加于其上,或者羁试器还是处理不了异常,系统就

调用ExitProcess函数来终结程序。

 

 

 (5)  在终结程序之前,系统会再次调用发生异常的线程中的所有异常处理过程,这是线程异常

处理过程所获得的清理未释放资源的最后机会,此后程序就终结了。

 

目录:

异常处理的基本概念

SEH的概念及基本知识

SEH异常处理程序原理与设计

向量化异常处理

x64平台上的异常处理

异常处理的实际应用

SEH的概念及基本知识

SEH(StructuredExceptionHandling,结构化异常处理)是Windows操作系统用于自身除错的一

种机制,也是开发人员处理程序错误或异常的强有力的武器。SEH是一种错误保护和修复机制,它

告诉系统当程序运行出现异常或错误时由谁来处理,给了应用程序一个改正错误的机会。从程序设

计的角度来说,就是系统在终结程序之前给程序提供的一个执行其预先设定的回调函数的机会。

2.1 SEH相关的数据结构

1.TIB结构

TIB(ThreadlnfomationBIock,线程信息块)是保存线程基本信息的数据结构。在用户模式下,

位于TEB(Environment,线程环境块)的头部,而TEB是操作系统为了保存每个线程

的私有数据创建的,每个线程都有自己的TEB。在Windows 2000 DDK中,TIB的定义如下。

typedef stzuct _NT_TIB{

Struct  EXCEPTION_REGISTRATION_RECORD *ExceptionList;//指向异常处理链表

PVOID  StackBase;   //当前线程所使用的栈的栈底

PVOID  StackLimit;  //当前线程所使用的栈的栈顶

PVOID  SubSystemTib;

union{

PVOID  FiberData;

VLONG  Version; 

 

};

PVOID  ArbitraryUserPointer; //指向TIB结构自身

struct  _NT_TIB  *Se1f;

}NT_TIB;

其中,与异常处理相关的项是指向EXCEPTION_REGISTRATION_RECORD结构的指针

LxceptionList,它位于TIB的偏移0处,同时在TEB的偏移0处。

在x86平台的用户模式下,Windows将FS段选择器指向当前线程的TEB数据,即TEB总是由fs:[0]指向的.(在x64平台上,这个指向关系变成了gs:[0] 关于x64平台上的异常处理,会在5节详细讲述,而当线程运行在内核模式

下时,Windows将段选择器指向内核中的KPCRB结构(ProcessorCon1RegionBlock.处理器控

制块),该结构的头部同样是上述的NT_TIB结构。

 

2._EXCEPTION_REGISTRATION_RECORD结构
TEB偏移量为0的_EXCEPTION_RECISTRATION_RECORD主要用于描述线程异常处理过程的
地址,多个该结构的链表描述了多个线程异常处理过程的嵌套层次关系,其定义如下。

typedef struct _EXCEPTION_REGISTRATION_RECORD {
  struct _EXCEPTION_REGISTRATION_RECORD *Next;//指向下一个结构的指针
  PEXCEPTION_ROUTINE Handler;    //当前异常处理回调函数的地址
) EXCEPTION_REGISTRATION_RECORD;

其中,"Next"是指向下一个-EXCErrION-REGISTRATION-RECORD(简称"ERR”)的指针,
形成一链状结构,而链表头就存放在fs:[01指向的TEB中;"HandIer”指向异常处理回调函数

当程序运行过程中产生异常时,系统的异常分发器就会从fs:[0]处取得异常处理的链头,然后查找异常处理链
表并依次调用各个链表节点中的异常处理回调函数。由于TEB是线程的私有数据结构,相应地,每个线程也都有自己的异常
处理链表,即SEH机制的作用范仅限于当前线程。从数据结构的角度来讲,SEH链就是一个只允许在链表头
部进行增加和删除节点操作的单向链表,且链表头部永远保存在fs:[0]处的TEB结构中。

3.EXCEPTION_RECORD结构和CONTEXT结构

这两个结构分别描述了异常发生的异常相关信息和线程状态信息。

 

4._EXCEPTION_POINTERS结构
前面,当一个异常发生时,在没有调试器干预的情况下,操系统会将异常信息转交给
用户态的异常处理过程。实际上,由于同一个线程在用户态和内核态使用的是两个不同的栈,为了
让用户态的异常处理程序能够访问与异常相关的数据,操作系统必须把与本次异常相关联的
EXCEPTION——RECORD结构和CONTEXT结构放到用户态栈中,同时在栈中放置一个_EXCEPTION_POINTTERS结构,

它包含两个指针,一个指向EXCEPTION_RECORD构,另一个指向CONTEXT.

struct EXCEPTION_POINTERS{
  PEXCEPTION_RECORD   ExceptionRecord;
  PCONTEXT  ContextRecord;
}EXCEPTION_POINTERS, *PEXCEPTION_POINTERS;

 

2.2 SEH处理程序的安装和卸载

根据SEH的设计要求,它的作用范围与安装它的函数相同,所以通常在函数头部安装SEH
异常处理程序,在函数返回前卸载。可以说,SEH是基于栈帧的异常处理机制
在安装SEH处理程序之前,需要准备一个符合SEH标准的回调函数,然后使用如下代码进行
SEH异常处理程序的安装。

assume fs:nothing ;是MASM编译器的特殊要求,若不满足该要求将出现编译错误
    push offset SEHandler ;相继向栈中压人了Handler和当前SEH链表头,这两个元素构成了一个新的—EXCEYMON-REGISTRATION-RECORD结构
    push fs:[0]
    mov fs:[0],esp   ;  把esp(也就是最新的链表头)保存到fs:[0]中

卸载只要把保存的fs:[0]填回并恢复栈平衡即可,相当于从链表头部删除一个节点

mov esp,dword ptr fs:[0] ;?????????????????????/没看懂
pop dword ptr fs:[0]

 

2.3 SEH实例跟踪

 

3. SEH异常处理程序原理及设计

3.1 异常分发过程

用户态的异常分发是从ntdll!KiUserExceptionDispatcher函数开始的。此时,栈中有EXCEITION_
RECORD和CONTEXT两个结构。该函数的圭要流程可以用以下代码表示。

KåUserExceptionDispatcher(PEXCEPTIONRECORDpExcptRec,CONTEXTpContext)
DWORDretVa1ue;
//Rt1DispatchException如果在执行过程中发生意外情况,将不会返回
if{Rt1DispatchException(pExceptRectpContext)
//如果异常被处理且返回值为继续执行,那么使用修复的CONTEXT恢复执行,NtContinue不会返回
retVaIue=NtContinue亻pContext,0)
eise
//如果异常没有被处理,将再次引发异常。注意第3个参数,FÄLSE表示第2次异常,该函数不会返回
retVa1ue二NtRaiseException(pExceptRectpContexttFALSE)
//如果执行到这里·说明以上过程再次发生异常,那么标记该异常不可继续执行
EXCEPTIONRECORDexcptRec2
excptRec2.ExceptionCode=retVa1ue;
excptRec2.ExceptionF1ags=EXCEPTIONNONCONTINUABLE
excptRec2.ExceptionRecord=pExcptRec;
excptRec2.NumberParameters=
RtIRaiseException(&excptRec2);
KiUserExceptionDispatcher()

从上面的代码中可以看出,ntdll!RtlDispatchException函数用来具体分发异常当异常被处理以
后,会使用NtContinue服务恢复该线程的执行,如果异常没有被处理,调用NtRaiseException函
数引发第2次异常(注意第3个参数BOOLEANFirstChance,"FAISE"示不是第1次机会),这
与前文介绍的异常处理过程是一致的。
ntdll!RtlDispatcbException函数的细节更复杂,在WRK(WindowsResearchKernel,微软开源的
用于操作系统研究的Windows2m3SP1内核部分源代码)中有它的内核部分同名函数nt!RtlDispatch
Exception的实现代码。至于用户部分的代码,其主要流程与内核中的处理基本一致,但仍有一些不
同。笔者参考了WRK中的相关代码,并对Windows7x86SP1中该函数的具体实现过程进行了逆向,
得到了ntdll!RtlDispatchException函数的完整代码,示例如下

在书本326页
BOOLEAN __stdcall RtlDispatchException(PEXCEPTION_RECORD pExcptRec,CONTEXT *pContext)

*RtlIsValidHandler:负责对SEHandler的安全性进行验证,是SafeSEH功能的具体实现部分。
*RtlpExecuteHandIerForException:负责执行SEHandIer.

如果抛开验证过程不谈,只看其核心流程,其实非常简单。异常分发的主要过程如下。
1.调用VEH ExceptionHandler进行异常处理,若返回继续执行,则直接返回,否则继续进行
SEH部分的处理。
2.遍历当前线程的异常链表,逐一调用RtlpExecuteHandlerForException。该函数会调用SEH异
常处理函数,根据不同的返回值进行不同的处理。
  *对ExceptionContinueExecution结束遍历并返回(对标记为EXCEPTION_NONCONTINUABLE
的异常不允许再次恢复执行,会调用RtlRaiseException)。
  *对ExceptionContionueSearch ,继续遍历下一个节点。
  *对ExceptionNestedException,从指定的新异常开始继续遍历。
只有正确处理才会返回"TRUE",其他情况都返回"FALSE”
3.调用VEH ContinueHandler进行异常处理。
如果读者没有完全理解以上过程也没关系,接下来还将进行详细的讲述。

3.2 线程异常处理

当异常发生时,异常现场被保存在发生了异常的线程的栈上,
系统从异常线程的TIB中取得SEH链表的表头,并遍历查找能够处理该异常的处理程序。所以,同
一进程中的A、B两个线程,A线程发生的异常是无法被B线程的异常理程序捕获的。
更进一步来讲,因为SEH的安装和卸载一般是在函数的头、尾进行,所以它的监视范围是由
函数调用关系确定的,作用范围是局部的,并且是基于栈帧的。一般来说,程序员在执行可能发生
异常的代码之前会安装异常处理程序。

1.线程异常处理的工作细节

SEH异常处理程序也就是回调函数的原型定义如下:

//异常处理程序的返回值
typedef enum  _EXCEPTION_DISPOSITION{
    ExceptionContinueExecution,    //0
    ExceptionContinueSearch,    //1
    ExceptionNestedException,    //2
    ExceptionCollidedUnwind    //3
EXCEPTION_DISPOSITION;

//回调函数原型
EXCEPTION_DISPOSITION __cdecle_except_handler(

    struct   _EXCEPTION_RECORD *ExceptionRecord,//指向前面介绍的包含异常处理信息的EXCEPTION_RECORD结构的地址。
    void  *    EstablisherFrame, //指向SEH链中当前_EXCEPTION_REGISTRATION结构的地址。///???????????????????????
    struct    _CONTEXT*      ContextRecord, //指向与线程相关的寄存器映像CONTEXT结构的地址。
    void *    DispatcherContext   //该域用于内嵌异常的处理,可以忽略它
);

各个参数的意义相当明确,而回调函数要做的就是通过EXCEEION_RECORD结构中的信息判
断当前异常是不是自己能够处理的。如果能,那么需要根据异常产生的原因进行相应的修正,必要
时会修改CONTEXT结构,然后恢复执行;如果不能,则告诉系统去寻找下一个处理程序。
返回值的定义及含义如下。
(1)ExceptionContinueExecution
一句话描述:“这个麻烦我已经帮你解决了,你回去重新执行一下试试。"
当恢复执行时,系统通过重新加载其传递给异常回调函数的CONTEXT相关信息来恢复线程的执行,
读者可通过修改CONTEXT的相关成员来改变返回后的执行地址及其他寄存器的内容。
如果回调函数并没有修复异常却返回了这个值,那么毫无疑问,会再次触发异常并进人一种无限循环状态。
(2)ExceptionContinueSearch

一句话描述:“这个麻烦我搞不定,你去找其他人看能不能解决吧!"
返回该值表示回调函数不能处理异常,需要用SEH回调函数的链表中的其他回调函数来处理。
实际上就是告诉系统,通过遍历SEH链表的后续节点去检测是否有其他异常处理程序能够处理该
异常。
(3)ExceptionNestedException
一句话描述:“我帮你解决麻烦的时候自岂也遇到麻烦了,现在自身难保。”
返回该值表示回调函数在试图处理该异常时再次发生了异常,也就是嵌套异常。这种情况是比
较糟糕的。如果这种情况发生在内核中,则会直接BugCheck,停止系统的运行。如果这种情况发生
在应用层,系统会尝试从嵌套异常的事发地点重新分发和处理嵌套异常。

(4)ExcepuonCollidedUnwind
一句话描述:“我在恢复事故现场时遇到麻烦了。”
返回该值表示回调函数在进行异常展开操作时再次发生了异常,其中展开操作可以简单理解为
恢复发生事故的第一现场,并在恢复过程中对系统资源进行回收。与上一个返回值一样,这也是非
常严重的错误。但是,由于展开操作一般是由系统在处理异常的过程中进行的,用户自定义的回调
函数通常不返回这个值。


一般来说,后两个返回值只见于系统内部的处理过程,用户自定义的回调函数只返回前两个值。
若返回值为ExceptionContinueExecution,则表示已经修复了错误,从原地址开始继续执行。这非常

有用·例如在发生除零错误时,我们可以将被除数修改为非零值,从而继续执行,如下面的实例:

在上例中,程序在注册异常回调函数之后故意制造了除零错误,这一错误将被注册的回调函数
捕捉。在回调函数中改变ECX的值,例如将其置为非零值,这样在继续执行程序时,系统蒋使用修

改后的CONTEXT值重新加载线程环境。这次除法运算不会产生异常,程序芷常执行,

 

2.线程回调函数的嵌套以及链表结构

当前线程中所有已经注册的EXCEPTION_REGISTRATION_RECORD结构构成了一单向链表。

 

 

 

fs:[0]总是指向最内层的异常处理回调过程,也就是最晚注册的异常回调。当异常发生时,系统
会先调用fs:[0]指向的最内层的异常回调过程。此时,回调函数有两种选择:如果返回值为
ExceptionContinueExecution,系统将控制权返回CONTEXT结构中EIP指定地址处的指令继续执行;
如果回调函数返回值为FxceptionContinueSearch,系统从当前EXCEPRION-RECISTRATION-RECORD
结构的Next域中找到对应临近的外层EXCEPTION-REGISTRATION-RECORD结构,进而找到对应的
异常处理回调函数地址并转向对它的调用。此过程可能持续下去,直到有异常处理过程处理该异常
为止。若Next的值为0FFFFFFFFh,则表示是链表的最后一个节点,该节点的回调函数是系统设置
的一个终结处理函数,所有无人处理的异常都会到达这里,在没有调试器存在的情况下,它总是会
选择处理异常.
下面通过一个实例MultiHandler.asm来加深理解。程序注册了3个回调函数,分别用于处理除
法错误、内存写冲突及INT3中断。在每个回调函数中,我们需要判断异常是否为回调函数能处理
的类型,如果是就处理并返回程序执行,如果不是就继续沿SEH链表查找,直到找到为止。由于最
后一条非法指令没有对应的回调函数处理,系统就用默认的处理过程处理,结果显示了非法操作对
话框并关闭了程序,代码如下。

3.3 异常处理的栈展开

1.什么是栈展开

传递给回调函数的EXCEPTION_RECORD结构的ExceptionFlags域有3个可选
值,分别是0、1和2。 0表示可修复异常,前面的例子都是可修复异常;1代表不可修复异常,这
在应用程序中不多见,只有在异常处理中又发生了异常或者系统内核发星严重错误时才可能导致这
种情况;2代表展开操作。
那么,究竟什么是展开操作呢?还是从系统对异常的处理顺序谈起,正像在1.2节用户态异
常处理的第5步中所描述的,当程序中所有的(如果有)异常回调函数(包括顶层异常回调函数)

都不处理异常时,系统在终结程序之前会给发生异常的线程中所有注册的回调函数一个调用。不同
的是,在调用之前要将EXCEPTION_RECORD结构中的ExceptionFlags域置为2,将ExceptionCode
域置为STATUS_UNWIND(0x0C0000027)。过个回调的自的是给它们一个清理的机会,例如释放重
要的系统资源、保存异常发生时关键变量的值等善后工作。

 

2.为什么要进行栈展开

一股情况下,只有在系统终结程序之前,栈展开才会发生,其最主要的目的是给程序清理未释
放资源的机会如果决定要自己处理大部分异常,并在处理后继续正常执行,也可以参照系统的设
计自己进行栈展开,给异常回调函数链表上的其他回调函数清理的机会。在自己的异常处理回调
函数中进行栈展开不是必需的,取决于程序员的选择和程序的设计。进行栈展开的另外一个理由
是,如果不进行栈展开操作,就有可能引发未知的错误,这取决于具体的设计实现。
例如,很多SEH使用者习惯于在处理某些错误后转到安全地址继续运行程序。为了转到安全地
址,需要保存安全的ESP指针,以便在处理错误后恢复正确的栈。通常会将安全的ESP值和
EXCEPTION_REGISTRATION结构一起保存在栈中,如图8.9所示是典型的做法。

 

 

再假设有如图所示的程序段及异常回调函数。共有3个函数,Fuc1调用Fuc2,Fuc2调用
Fuc3,其对应的异常回调函数的监视范围如线框所示。

 

 

假设其EXCETION_REGISTRATION结构及安全ESP均按照前面介绍的典型做法压人栈。现在
看看假如在指令执行过程中在Fue3发生了异常,情形会怎样。假设无法处理,因此它返
回ExceptionContinueSearch.系统继续遍历SEH链表,控制权交给了Handler2也无法处
理,同样返回ExceptionContinueSearch.系统继续遍历,控制权交给了Handler1。Handler1认为已经
不能在原异常处继续恢复执行,需要转到安全处继续执行,因此程序控制返回Fuc1的Safe标号处继续执行。

到现在为止似乎一切正常,但假如在safe处发生了异常2,问题就可能出现了。问题在何处?
还是先看看栈的变化吧。显然,在异常发生时,栈的位置如图中的(1)所示,fs:[0]指向包含
Handler3的EXCFMION_REGISTRATION结构。然而,由于在Handler1中将EIP转到S处执行,
其对应栈顶如图8.11中的(2)所示。这时如果再发生异常,由于f可0]仍指向Handler3,依然会进行
SEH链表遍历过程。这好像没错,可是你是否意识到,如果在Safe处后行了push(压栈)等操作,
破坏了本该是Handler3和Handler2的值,那么fs:[0]指向的将是一个无效结构,也就是说,错误在
所难免。而这种错误是潜在的,可能很难调试。

 

 

在外层回调函数改变了执行地址(转到安全地址)及栈指针的情况可能会造成潜在的错误。
为了消除这种潜在的错误,需要进行栈展开,步骤如下。
  1 将EXCEVTION_RECORD结构中的ExceptionFlags域置为2,将ExceptionCode域置为
STATUS_UNWIND(0x0C0000027),并从fs:[0]指向的回调函数开始,SEH链表依次调用各回调
函数,到引发调用的SEH回调函数为止〈不包括该回调函数)。让它们完成清理工作,包括转储必
需的关键运行时参数值及错误信息映像,以便进行后面的调试等操作。
  2 基于上述原因,将fs:[0]调整为指向当前引发调用的回调函数所对应的EXCEPTION_
REGISTRATION结构,从而避免发生潜在错误。


3 如何进行栈展开
进行栈展开的方法很简单,微软提供了现成的API函数RtlUnwind;其调用方式如下(来自MSDN).

RtIUnwind(VirtualTargetFrame,TargetPC,ExceptionRecord,ReturnValue)

*VinualTargetFrame:展开时,最后在SEH链上停止于回调函数所对应的EXCEYPION_
REGISTRATION的指针,即希望在哪个回调函数前展开调用停止,其对应的EXCEPTION_
REGISTRATION结构的指针就作为该参数使用(在大部分情况下是引发调用的回调函数所
对应的EXCEITION_REGISTRATION结构的指针,也可以不是)。
*TargetPC:调用RtlUnwind返回后应执行指令的地址。如果为0,则自然返回RtlUnwind调用
后的下一条指令,与正常的API调用相同。
*ExceptionRecord:当前异常的EXCEITION_RECORD结构,可以直接使用在异常中传递给回
调函数的该参数。
*ReturnValue:返回值,通常不使用。

  MSDN提供的参数不是那么容易理解,不过解释之后还是非常简单的。与其他API自动保存
ebx、esi和寄存器不同,RtlUnwind并不自动保存这些寄存器,所以在调用该API之前要注意保
护。演示栈展开的实例UnWind.asm如下。

 

本例的大部分内容和3.2节的实例MultiHandler.asm一样,只是增加了对非法指令异常的处理,
可以选择自己展开代码或让系统展开,命令如下。
InstSEHframe SELF_UNWIND

  本例与实例MultiHandler.asm不同的是,在每个异常回调函数中加人了对展开情况的处理,处
理方法也很简单,只是显示一个消息框而已。应该在系统中运行并观察运行结果,最好能用调试工
具加以分析,以明确其执行流程。
  按照上面的描述进行手动展开也是很简单的,但毫无疑问,如无特殊需要,没有必要重复“发
明轮子",最好利用现成的API函数。

3.4 MSC编译器对线性异常处理的增强

在上面的例子中,我们一直使用汇编语言来编写异常处理程序,向读者展示了异常处理最基本
的原理。但是,这个操作过程是极其不便的,尤其对高级语言来说,直接操作寄存器、读写fs:[0]并
不合适且非常烦琐。为此,各主流编译器都对SEH机制进行了扩充和增强,使程序员能更简便地使
用异常处理机制。所以,在现实程序设计中,除了保护壳、反调试等特殊用途,基本上没有直接使
用系统的SEH机制,而是使用编译器提供的增强版本。C语言是Windos操作系统的开发语言,下
面笔者就继续带领大家了解微软的C编译器(MSC)提供的增强版本异常处理机制

1.增强的数据结构及定义

2.编译器的SEH增强设计

3.编译器的实际工作

4._except_handler3函数流程解析

5.最新版本编译器实现

6.C++异常处理

3.5 顶层异常处理

3.6 异常处理程序的安全性

4 向量化异常处理

4.1向量化异常处理的使用

4.2 VEH与SEH的异同

4.3向量化异常处理的新内容

5 x64平台上的异常处理

5.1 原生x64程序的异常分发

5.2 WOW64下的异常分发

6 异常处理程序设计中的注意事项

7 异常处理的实际应用

7.1 使用SEH对用户输入进行验证

7.2 SEH在加密与解密中的应用

7.3 用VEH实现API Hook

 

posted @ 2021-03-06 18:26  KnowledgePorter  阅读(467)  评论(0)    收藏  举报