23.1 基础知识
23.1.1 Windows下的软件异常
(1)中断和异常
①中断是由外部硬件设备或异步事件产生的
②异常是由内部事件产生的,可分为故障、陷阱和终止三类。
(2)两种异常处理机制:SEH和VEH(WindowsXP以上新引进)
(3)结构化异常处理(SEH)是Windows操作系统提供的强大异常处理功能。而Visual C++中的__try{}/__finally{}和__try{}/__except{}结构本质上是对Windows提供的SEH的封装。
23.1.2 SEH的分类
(1)Per-Thread类型SEH(也称为线程异常处理),用来监视某线程代码是否发生异常。
(2)Final类型SEH(也称为进程异常处理、筛选器或顶层异常处理),用于监视整个进程中所有线程是否发生异常。在整个进程中,该类型异常处理过程只有一个,可通过SetUnhandledExceptionFilter设置。
23.1.3 SEH相关的数据结构
(1) 线程信息块TIB(Thread Information Block或TEB)
typedef struct _NT_TIB { struct _EXCEPTION_REGISTRATION_RECORD *ExceptionList; //异常的链表 PVOID StackBase; PVOID StackLimit; PVOID SubSystemTib; union { PVOID FiberData; DWORD Version; }; PVOID ArbitraryUserPointer; struct _NT_TIB *Self; } NT_TIB;
Fs:[0]总是指向当前线程的TIB,其中0偏移的指向线程的异常链表,即ExceptionList是指向异常处理链表(EXCEPTION_REGISTRATION结构)的一个指针。
(2)EXCEPTION_REGISTRATION结构
typedef struct _EXCEPTION_REGISTRATION_RECORD { struct _EXCEPTION_REGISTRATION_RECORD *Prev; //指向前一个EXCEPTION_REGISTRATION的指针 PEXCEPTION_ROUTINE Handler; //当前异常处理回调函数的地址 } EXCEPTION_REGISTRATION_RECORD;
(3)EXCEPTION_RECORD结构
typedef struct _EXCEPTION_RECORD { DWORD ExceptionCode; //异常码,以STATUS_或EXCEPTION_开头,可自定义。(sehdef.inc) DWORD ExceptionFlags; //异常标志。0可修复;1不可修复;2正在展开,不要试图修复 struct _EXCEPTION_RECORD *ExceptionRecord; //指向嵌套的异常结构,通常是异常中又引发异常 PVOID ExceptionAddress; //异常发生的地址 DWORD NumberParameters; //下面ExceptionInformation所含有的dword数目 ULONG_PTR ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS]; //附加消息,如读或写冲突 } EXCEPTION_RECORD;
(4)CONTEXT结构(可通过GetThreadContext和SetThreadContext来读写)
typedef struct _CONTEXT { DWORD ContextFlags; //用来表示该结构中的哪些域有效 DWORD Dr0, Dr2, Dr3, Dr4, Dr5, Dr6, Dr7; //调试寄存器 FLOATING_SAVE_AREA FloatSave; //浮点寄存器区 DWORD SegGs, SegFs, SegEs, Seg Ds; //段寄存器 DWORD Edi, Esi, Ebx, Edx, Ecx, Eax; //通用寄存器组 DWORD Ebp, Eip, SegCs, EFlags, Esp, SegSs; //控制寄存器组 //扩展寄存器,只有特定的处理器才有 BYTE ExtendedRegisters[MAXIMUM_SUPPORTED_EXTENSION]; } CONTEXT;
(5)EXCEPTION_POINTERS结构(传递给顶层型异常处理回调函数的参数)
typedef struct _EXCEPTION_POINTERS { PEXCEPTION_RECORD ExceptionRecord; //指向ExceptionRecord的指针 PCONTEXT ContextRecord; //指向Context结构的指针 } EXCEPTION_POINTERS, *PEXCEPTION_POINTERS;
23.1.4 异常处理相关API
(1)SetErrorMode:控制错误模式,如是否出现错误对话框
(2)SetUnhandledExceptionFilter:设定顶层异常处理回调函数
(3)RaiseException:用于引发异常,可指定自己的异常代码及相关信息
(4)GetThreadContext/SetThreadContext:获取或设置线程环境
(5)RtlUnwind:栈展开操作(后面会详细介绍)
23.1.5 顶层异常处理
(1)顶层异常处理是进程相关的,只要没有由线程处理过程或调试器处理掉,最终均要交由顶层异常回调函数处理
(2)注册顶层异常处理:SetUnhandledExceptionFilter,每个进程只能注册一个。返回值为前一个异常回调函数的地址。
(3)回调函数
LONG UnhandledExceptionFilter(STRUCT _EXCEPTION_POINTERS *ExceptionInfo){ //三种返回值,决定系统下一步的动作 //1.EXCEPTION_EXECUTE_HANDLE(1):表示异常己被处理。程序可以优雅结束 //2.EXCEPTION_CONTINUE_SEARCH(0):表示顶层异常处理不能处理,需交给其他异常处理过程 //3.EXCEPTION_CONTINUE_EXECUTION(-1):表示顶层异常过程处理了异常,且程序应从原异常发生的指令重新继续执行一下。(可以通过改变CONTEXT内容来达到改变程序执行环境) }
23.1.6 线程异常处理(局部的,仅仅监视进程中某特定线程是否发生异常)
(1)线程异常处理特点
①Windows系统为每个线程单独提供了一种异常处理的方法,当一个线程出现错误时,操作系统调用用户定义的一系列回调函数,在这些回调函数中,可以进行修复错误或其它的一些操作,最后的返回值告系统系统下一步的动作(如继续搜索异常处理程序或终止程序等)。
②SEH是基于线程的,使用SEH可以为每个线程设置不同的异常处理程序(回调函数)而且可以为每个线程设置多个异常处理程序。
③ 由于SEH使用了与硬件平台相关的数据指针,所以不同硬件平台使用SHE的方法有所不同。
(2)回调函数原型
EXCEPTION_DISPOSITION __cdecl _except_handler(
struct _EXCEPTION_RECORD *ExceptionRecord,//指向包含异常信息的EXCEPTION_RECORD结构 void* EstablisherFrame,//指向该异常相关的EXCEPTION_REGISTRATION结构 struct _CONTEXT *ContextRecord,//指向线程环境CONTEXT结构的指针 void* DispatcherContext){ //该域暂无意义 …… //4种返回值及含义 //1.ExceptionContinueExecution(0):回调函数处理了异常,可以从异常发生的指令处重新执行。 //2.ExceptionContinueSearch(1):回调函数不能处理该异常,需要要SEH链中的其他回调函数处理。 //3.ExceptionNestedException(2):回调函数在执行中又发生了新的异常,即发生了嵌套异常 //4.ExceptionCollidedUnwind(3):发生了嵌套的展开操作 return … }
(3)线程异常处理的注册
push _exception_handler //异常回调函数_exception_handler的地址,即handler push fs:[0] //保存前一个异常回调函数的地址,即prev mov fs:[0],esp //安装新的EXCEPTION_REGISTRATION结构(两个成员:prev,handler)。 //此时栈顶分别是prev和handler,为新的EXCEPTION_REGISTRATION结 //构,mov fs:[0],esp,就可以让fs:[0]指向该指构。
(4)异常回调函数的调用过程
①线程信息块(TIB),永远放在fs段选择器指定的数据段的0偏移处,即fs:[0]的地方就是TIB结构。对不同的线程fs寄存器的内容有所有不同,但fs:[0]都是指向当前线程的TIB结构体,所以fs:[0]是一个EXCEPTION_REGISTRATION结构体的指针。
②当异常发生时,系统从fs:[0]指向的内存地址处取出ExceptionList字段,然后从ExceptionList字段指向的EXCEPTION_REGISTRATION结构中取出handler字段,并根据其中的地址去调用异常处理程序(回调函数)。
【MySeh程序】安装线程异常处理程序
/************************************************************************ MYSEH - Matt Pietrek 1997 Microsoft Systems Journal,January 1997 cl.exe myseh.cpp /link -SAFESEH:NO 用IDE编译时须将项目的“链接器”→“高级”→“映像具有安全异常处理程序”设为SAFESEH:NO(Debug或Release版要分别设置) 用命令行CL MYSHE.CPP编译里可参考下面的MySeh.bat批处理的设置。 ************************************************************************/ //MySeh.bat //set PATH = %PATH%; E:\读书笔记\Windows核心编程\SEH\SEH; D:\VisualStudio\VC\bin //set include = C:\Program Files\Windows Kits\8.1\Include\um; C:\Program Files\Windows Kits\8.1\Include\shared; D:\VisualStudio\VC\include //set lib = C:\Program Files\Windows Kits\8.1\Lib\winv6.3\um\x86; D:\VisualStudio\VC\lib //cl.exe E : \读书笔记\Windows核心编程\SEH\SEH\myseh.cpp / link - SAFESEH:NO #define WIN32_LEAN_AND_MEAN #include <windows.h> #include <stdio.h> #include <stdlib.h> DWORD scratch; EXCEPTION_DISPOSITION __cdecl _except_handler(struct _EXCEPTION_RECORD *ExceptionRecord, void* EstablisherFrame, struct _CONTEXT *ContextRecord, void* DispatcherContext){ //指明是我们让流程转到我们的异常处理程序的 printf("进入Seh异常处理程序!\n"); //改变CONTEXT结构体EAX的值,以便它指向可以成功进行写操作的位置 ContextRecord->Eax = (DWORD)&scratch; printf("异常处理完毕!\n"); //告诉操作系统重新执行出错的指令 return ExceptionContinueExecution; } int main(){ DWORD handler = (DWORD)_except_handler; printf("安装seh异常回调函数!\n"); __asm{ //创建EXCEPTION_REGISTRATION结构: push handler //handler函数的地址 push fs:[0] //保存前一个handler函数地址,即EXCEPTION_REGISTRATION结构的prev mov fs:[0],ESP //安装新的EXCEPTION_REGISTRATION结构 } printf("安装完毕,向0地址写入数据,引发异常!\n"); __asm { xor eax, eax //EAX = 0 mov dword ptr[eax], 1234h //写EAX指向的内存从而故意引发一个异常! } printf("写入数据完毕,scratch=0x%x!\n",scratch); printf("卸载seh自定义的异常回调函数!\n"); __asm{ // 移去我们的EXECEPTION_REGISTRATION结构 mov eax, [ESP] // 此时栈顶为prev,后进先出原理。获取前一个EXECEPTION_REGISTRATION结构的指针 mov fs:[0], EAX // 安装前一个结构 add esp, 8 // 将我们的EXECEPTION_REGISTRATION弹出堆栈 } system("Pause"); return 0; }
(5)SEH链及异常的传递(通知调试器→SEH链→顶层异常处理→系统默认处理)
(1)系统查看产生异常的进程是否被正在被调试,如果正在被调试,那么向调试器发送EXCEPTION_DEBUG_EVENT事件。
(2)如果进程没有没有被调试或者调试器不去处理这个异常,那么系统检查异常所处的线程并在这个线程环境中查看fs:[0]来确定是否安装SEH异常处理回调函数,如果有则调用它。
(3)回调函数尝试处理这个异常,如果可以正确处理的话,则修正错误并将返回值设置为ExceptionContinueExecution,这时系统将结束整个查找过程。
(4)如果回调函数返回ExceptionContinueSearch,相当于告诉系统它无法处理这个异常,系统将根据SEH链中的prev字段得到前一个回调函数地址并重复步骤3,直至链中的某个回调函数返回ExceptionContinueExection为止,查找结束。
(5)如果到了SEH链的尾部却没有一个回调函数愿意处理这个异常,那么系统会再被检查进程是否正在被调试,如果被调试的话,则再一次通知调试器。
(6)如果调试器还是不去处理这个异常或进程没有被调试,那么系统检查有没有Final型的异常处理回调函数,如果有,就去调用它,当这个回调函数返回时,系统会根据这个函数的返回值做相应的动作。
(7)如果没有安装Final型回调函数,系统直接调用默认的异常处理程序终止进程,但在终止之前,系统再次调用发生异常的线程中的所有异常处理过程,目的是让线程异常处理过程获得最后清理未释放资源的机会,其后程序终止。
【MySEH2】SEH链及异常传递
①当使用SEH时,如果一个函数有一个异常处理程序但它却不处理某个异常,这个函数就有可能非正常退出。例如在MYSEH2中 HomeGrownFrame函数就不处理异常。由于在链表中后面的某个异常处理程序(这里是main函数中的)处理了这个异常,因此出错指令后面的 printf就永远不会执行,即第52行。
②比较开头输出的两行。第一次异常标志是0,而第二次是2。这就是堆栈展开(Unwinding)。实际上,当一个异常处理回调函数拒绝处理某个异常时,它会被再一次调用。
/************************************************************************ MYSEH2 - Matt pietrek 1997 Microsoft Systems Journal,january 1997 File: MySEH2.cpp 用IDE编译时须将项目的“链接器”→“高级”→“映像具有安全异常处理程序”设为SAFESEH:NO(Debug或Release版要分别设置) ************************************************************************/ #include <windows.h> #include <stdio.h> //异常处理回调函数 EXCEPTION_DISPOSITION __cdecl _except_handler( struct _EXCEPTION_RECORD *ExceptionRecord, void * EstablisherFrame, struct _CONTEXT *ContextRecord, void * DispatcherContext){ //显示异常信息 printf("HomeGrown异常处理回调函数:异常码(%08X) 标志(%X)", ExceptionRecord->ExceptionCode,ExceptionRecord->ExceptionFlags); if (ExceptionRecord->ExceptionFlags & 1) printf(" EH_NONCONTINUABLE"); if (ExceptionRecord->ExceptionFlags & 2) printf(" EH_UNWINDING"); if (ExceptionRecord->ExceptionFlags & 4) printf(" EH_EXIT_UNWIND"); if (ExceptionRecord->ExceptionFlags & 8) // printf(" EH_STACK_INVALID"); if (ExceptionRecord->ExceptionFlags & 0x10) // printf(" EH_NESTED_CALL"); printf("\n"); //我们不想处理这个异常,让其它SEH链的其他回调函数去处理 return ExceptionContinueSearch; } void HomeGrownFrame(void){ DWORD handler = (DWORD)_except_handler; __asm{ //创建EXCEPTION_REGISTRATION结构 push handler //handler函数的地址 push FS :[0] //保存前一个handler地址 mov FS :[0],ESP //安装新的EXCEPTION_REGISTRATION结构 } *(PDWORD)0 = 0; //向地址0数据,从而引发一个异常 printf("_except_handle函数不处理异常,此句永远不会被执行!\n"); __asm{ //移去我个的EXCEPTION_REGISTRATION结构 mov eax, [ESP] //获取前一个结构 mov FS : [0],eax //安装前一个结构 add esp,8 //把我们的EXCEPTION_REGISTRATION结构弹出 } } int main(){ //为了简化代码,使用编译器层面的异常处理,即__try{} __excetp{} __try{ HomeGrownFrame(); } __except (EXCEPTION_EXECUTE_HANDLER){ //这里处理了异常,不再向外层传递 printf("主函数main中的异常处理过程\n"); } return 0; }
23.1.7 异常处理的堆栈展开(Stack Unwind)
(1)什么是展开操作
①当发生异常时,系统遍历EXCEPTION_REGISTRATION结构链表,从链表头,直到它找到一个处理这个异常的处理程序。一旦找到,系统就再次重新遍历这个链表,直到处理这个异常的节点为止(即返回ExceptionContinueExecution节点)。在这第二次遍历中,系统将再次调用每个异常处理函数。关键的区别是,在第二次调用中,异常标志被设置为2。这个值被定义 为EH_UNWINDING
②注意展开操作是发生在出现异常时,这个异常回调函数拒绝处理的时候(即返回ExceptionContinueSearch)。这里系统会从链表头开始遍历(因异常的嵌套,可理解为由内层向外层),所以各异常回调函数会第1次被依次调用,直到找到同意处理的节点。然后,再重新从链表头开始(即由内向外)第2次调用以前那些曾经不处理异常的节点,直到同意处理的那个异常的节点为止。
③当一个异常处理程序拒绝处理某个异常时,它实际上也就无权决定流程最终将从何处恢复。只有处理某个异常的异常处理程序才能决定待所有异常处理代码执行完毕之后流程最终将从何处恢复。即,当异常已经被处理完毕,并且所有前面的异常帧都已经被展开之后,流程从处理异常的那个回调函数决定的地方开始继续执行。
④展开操作完成后,同意处理异常的回调函数也必须负责把Fs:[0]恢复到处理这个异常的EXCEPTION_REGISTRATION上,即展开操作导致堆栈上处理异常的帧以下的堆栈区域上的所有内容都被移除了,这个异常处理也就成了SEH链表的第1个节点。
(2)为什么要进行堆栈展开
①第1个原因是告知回调函数将被卸掉,以让被卸载的回调函数有机会进行一些清理未释放资源的机会。因为一个函数发生异常时,执行流程通常不会从这个函数正常退出。所以可以导致资源未被正确释放(如C++类的对象析构函数没被调用等)。
②第2个原因是如果不进行堆栈展开,可能会引发未知的错误。(见《软件加密技术内幕》第132-134页)。
(3)如何展开:RtlUnwind(lpLastStackFrame,lpCodelabel,lpExceptionRecord,dwRet);
①lpLastStackFrame:当遍历到这个帧时就停止展开异常帧。为NULL时表示展开所有回调函数。
②lpCodeLabel:指向该函数返回的位置。如果指定为NULL,表示函数使用正常的方式返回。
③lpExceptionRecord:指定一个EXCPETION_RECORD结构。这个结构将在展开操作的时候被传给每一个被调用的回调函数,一般建议使用NULL来让系统自动生成代表展开操作的EXCEPTION_RECORD结构。
④dwRet一般不被使用。
【注意】:
①MySEH2程序中我们并没有在main函数中的__try{}的 __except{}中调用RtlUnwind,是因为编译器在生成__try{}/__except{}时,己经帮我们做好了。否则如果是利用Windows本身的SEH来写的话,应该在返回ExceptionContinueExecution的回调函数中调用RtlUnwind来展开。
②RtlUnwind这个函数并不像其他API函数会保存esi、edi和ebx等寄存器,在函数返回的时候,这些寄存器的值也可能会被改变。如果程序用到了这些寄存器的话,要自己保存和恢复它们。