使用筛选器和SEH处理异常
Posted hznhh
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了使用筛选器和SEH处理异常相关的知识,希望对你有一定的参考价值。
平时程序发生异常都是系统来处理的,但是Windows其实也允许让我们自己来处理异常。第一种方法就是使用筛选器处理异常。
筛选器处理异常的方式是指定一个异常回调函数,当程序发生异常的时候,系统就会去调用这个函数,然后在函数里面我们可以自己来处理这个异常,可以选择退出或者是跳转到安全的地方执行代码。或者想干其他的也行。要注意,这个回调函数在进程范围内是唯一的,如果设置了很多回调函数,只会以最新的那个为准。
使用筛选器处理异常
注册筛选器异常回调函数的函数是SetUnhandledExceptionFilter。
invoke SetUnhandledExceptionFilter,offset Handler
mov lpPrevHandler,eax
该函数唯一的参数就是异常回调函数的地址。函数执行成功返回值是上一次设置的回调函数的入口地址。如果参数是NULL,那么发生异常时,系统会直接把这个一异常送到系统默认处理的地方。
异常回调函数的写法也有格式规定。
_Handler proc lpExceptionInfo
函数只有一个参数,这个参数是系统传给它的。该参数是一个指针,指向一个EXCEPTION_POINTERS结构,该结构包含了产生异常的原因和详细的信息。该结构定义如下:
EXCEPTION_POINTERS STRUCT pExceptionRecord DWORD ? ContextRecord DWORD ? EXCEPTION_POINTERS ENDS
该参数只有两个字段,但是两个字段又分别指向不同的结构,第一个字段指向一个EXCEPTION_RECORD结构,该结构包含了异常的原因以及发生的位置等。结构的定义如下:
EXCEPTION_RECORD STRUCT ExceptionCode DWORD ? ;异常事件码 ExceptionFlags DWORD ? ;标志 pExceptionRecord DWORD ? ;下一个EXCEPTION_RECORD结构地址 ExceptionAddress DWORD ? NumberParameters DWORD ? ExceptionInformation DWORD ? EXCEPTION_RECORD ENDS
该结构的第一个字段是一个异常事件码。它指定了异常发生的原因。异常发生的原因都已经被预定成了一系列以EXCEPTION_开头或者以STATUS_开头的常量。异常原因码代表的原因可以在MSDN中查看。
后面一个字段指向一个CONTEXT结构,该结构存储的就是线程的环境,对一个线程来说,线程环境其实就是寄存器的状态,所以在系统传给回调函数的参数里面的这个结构里面存储的,其实就是异常发生时的寄存器状态。
1 CONTEXT STRUCT 2 ContextFlags DWORD ? 3 iDr0 DWORD ? 4 iDr1 DWORD ? 5 iDr2 DWORD ? ; 调试寄存器 6 iDr3 DWORD ? 7 iDr6 DWORD ? 8 iDr7 DWORD ? 9 FloatSave FLOATING_SAVE_AREA <> ;浮点寄存器区 10 regGs DWORD ? 11 regFs DWORD ? 12 regEs DWORD ? 13 regDs DWORD ? 14 regEdi DWORD ? 15 regEsi DWORD ? 16 regEbx DWORD ? 17 regEdx DWORD ? 18 regEcx DWORD ? 19 regEax DWORD ? 20 regEbp DWORD ? 21 regEip DWORD ? 22 regCs DWORD ? 23 regFlag DWORD ? 24 regEsp DWORD ? 25 regSs DWORD ? 26 ExtendedRegisters db MAXIMUM_SUPPORTED_EXTENSION dup(?) 27 CONTEXT ENDS
该结构也可以在MSDN中查看。
在函数中,我们可以做我们想做的事。但是最后的返回值要按照规定来,最后的返回值有三种:
- EXCEPTION_EXECUTE_HANDLER:该参数被定义为1,返回这个参数,进程会被终止,但是不会弹出错误提示对话框。
- EXCEPTION_CONTINUE_SEARCH:该参数被定义为0,返回这个参数,进程也会被终止,但是在终止之前会弹出出错的对话框。 返回以上这两个参数,基本上我们在函数里面不用做什么,做一些扫尾工作就可以了。
- EXCEPTION_CONTINUE_EXECUTION:该参数定义为-1,返回这个参数,则系统不会终止进程,他会将CONTEXT结构给设置回去,也就是说我们可以在函数中修改CONTEXT结构中寄存器的状态,然后返回这个参数时,系统就会把我们修改后的寄存器状态设置为当前寄存器的状态。
所以我们可以采用返回第三种参数的方式,在函数里面将CONTEXT结构里面的EIP寄存器改为安全位置的地址,然后函数执行完后就会直接返回到我们的安全位置去执行。
使用SEH处理异常
前一种用筛选器处理异常的办法虽然简单,但是也有它的局限性。筛选器处理异常的回调函数在整个异常中只能有一个,也就是说如果一个进程又很多个线程,这些线程的异常处理回调函数也就只有这一个,它无法专门给某个线程或者模块处理异常。但是SEH就可以,相对来说也麻烦一点。
同样的,使用SEH来处理异常也要注册回调函数。但是不同的是它的注册方式。注册SEH异常处理回调函数不像筛选器处理异常回调函数是用一个函数来完成的,它是通过FS段寄存器来安装的。
要知道怎么使用SEH异常处理,就要先了解一下它的原理。WIN32为每个线程定义了一个结构,这个结构叫NT_TIB。该结构定义如下:
NT_TIB STRUCT ExceptionList dd ? ;SEH链入口 StackBase dd ? ;堆栈基址 StackLimit dd ? ;堆栈大小 SubSystemTib dd ? FiberData dd ? ArbitraryUserPointer dd ? Self dd ? NT_TIB ENDS
这个结构里面最主要的就是第一个字段,这个字段又指向一个,这个结构的名字叫EXCEPTION_REGISTRATION,这个结构就重要了,我们的异常回调函数的地址就要放里面。该结构的定义如下:
EXCEPTION_REGISTRATION STRUCT prev dd ? handler dd ? EXCEPTION_REGISTRATION ENDS
结构的第一个字段是前一个EXCEPTION_REGISTRATION结构的地址,第二个参数就是我们的异常处理回调函数的地址。因为这个结构里面存着上一个结构的地址,上一个结构又存着上上个结构的地址,所以这样就形成了一条链,这个就是SEH链,这个概念待会再介绍。
所以我们要注册一个SEH异常处理回调函数就只用把函数地址给填到这个结构里就行了。但是这个结构在哪呢?答案就是FS段寄存器。NT_TIB结构永远存放在FS段寄存器指定的数据段的0偏移处。也就是说FS:[0]这个地方就是NT_TIB结构的起始位置,而这个结构的起始位置也就是该结构的ExceptionList字段的位置,也就是EXCEPTION_REGISTRATION结构的地址。
所以一般我们这样注册回调函数:
push offset _Handler push fs:[0] mov fs:[0],esp
第一步,先把异常处理回调函数的地址压入栈,第二步,将FS:[0]处的数据,也就是NT__TIB结构的第一个字段所指向的结构EXCEPTION_REGISTRATION的地址压入栈。然后第三步,把寄存器ESP的值赋值给FS:[0]。
原来的时候FS:[0]所在的地方是ExceptionList字段,这个地方放的数据,就是EXCEPTION_REGISTRATION结构的地址。这个地址后面被压入ESP所指向的地方,然后我们最后把ESP的值(ESP里面的数据是一个地址,这个地址所指向的地方就是之前被压入栈的EXCEPTION_REGISTRATION结构的地址)放入FS:[0]的地方,这里存放的就是EXCEPTION_REGISTRATION结构的地址,而现在这里是ESP的值,也就是说此刻ESP的值就是EXCEPTION_REGISTRATION结构的地址。也就是说[esp]是原来的结构的地址,esp的值是现在这个新的结构的地址。
这样一来,ESP以及ESP+4这两个位置就构成了一个新的EXCEPTION_REGISTRATION结构。ESP对应的就是字段prev位置,然后ESP+4对应的就是字段handler的位置 。
异常发生的时候,系统就会从FS:[0]中取出EXCEPTION_REGISTRATION结构的地址,也就是在完成上面三步操作后ESP的值([ESP]的地方是原来的EXCEPTION_REGISTRATION结构的地址)。然后再从[ESP+4]的地方取得回调函数的地址。
当不需要回调函数之后,要把FS:[0]恢复成之前的数据,也就是恢复原来的SEH链,还要平衡一下堆栈:
pop fs:[0] pop eax
第二条指令就是为了恢复堆栈。
了解了怎么注册SEH异常处理的回调函数之后,就应该了解SEH的回调函数怎么写了。它的回调函数和之前的筛选器异常处理回调函数不一样,它的参数就比较多了。函数的声明格式一般如下定义:
_Hnadler proc C _lpExceptionRecord,_lpSEH,_lpContext,_lpDispatcherContext
细心就会注意到,该函数的传参方式用的是C格式的而不是stdcall格式的。但是这个对我们使用它没有什么影响,因为是系统来调用它的。
这里对我们有用的就是前三个参数,一般也就只用前三给参数,所以这里只介绍一下前三个参数。
- _lpExceptionRecord:指向一个EXCEPTION_RECORD结构,这个结构前面有写。
- _lpSEH:这个参数的值就是在我们完成SEH异常处理回调函数的注册之后ESP寄存器中的值。这个参数可以被用来恢复发生异常前的堆栈。因为这个参数就是异常发生前的ESP的值。并且,我们还可以在注册回调函数的时候压入我们需要的参数,之后通过这个参数就可以获取之前我们压入的参数。
- _lpDispatcherContext:这个参数指向CONTEXT结构,这个结构前面也有讲。
这个函数的参数大概就是这样。但是我们之前有说可以让异常发生后程序跳转到安全的地方去执行。所以我们应该把安全位置的地址也传入函数数中,这个时候就可以用到第二个参数了。
如果要传额外的参数,那么在注册回调函数的的时候就要这样做:
push ebp ;额外的参数 push offset _SafePlace ;额外的参数 push offset _ Handler push fs:[0] mov fs:[0],esp
这样操作之后,我们在回调函数里面取出参数_lpSEH的值,然后这个值加加8的地方,就是安全位置的地址。所以在回调函数里面一般这样写:
_Handler proc C _lpExceptionRecord,_lpSEH,_lpContext,_lpDispatcherContext pushad mov esi,_lpExceptionRecord mov edi,lpContext assume esi:ptr EXCEPTION_RECORD,edi:ptr CONTEXT mov eax,_lpSEH push [eax + 8] ;入栈的就是安全位置的地址 pop [edi].regEip push [eax + 0ch] ;原来的ebp的值 pop [edi].regEbp push eax pop [edi].regEsp ;原来的esp的位置 assume esi:nothing,edi:nothing popad mov eax,ExceptionContinueExecution ret _Handler endp
在函数里面,我们将线程环境,修改为我们需要的。也就是把CONTEXT结构里的regEip改成安全位置的地址,对应的就是之前注册回调函数第二个压入栈的数据。这样当函数返回时,就会将EIP设置为这个值,然后就正好从安全位置开始执行。
然后我们还把原来的ESP和EBP的值都设置了,那样函数返回时线程环境就是异常发生前的样子了。
SEH异常处理回调函数的返回值有4个:
- ExceptionContinueExecution:等于0,回调函数返回后,系统将线程环境设置为_lpContext参数指定的CONTEXT结构并继续执行。
- ExceptionContinueSearch:等于1,返回这个值就表示我们写的回调函数不处理这个异常,把这个异常交给系统处理。系统会通过EXCEPTION_REGISTRATION结构的第一个字段指向的前一个结构中得到之前的回调函数的地址。
- ExceptionNestedException:等于2,回调函数在执行的时候又发生了异常。
- ExceptionCollidedUnwind:等于3,发生了嵌套的展开操作。
SEH链和异常的传递
之前有说到,SEH异常处理回调函数可以为每个线程或者模块都存在一个。但是它们的注册都是由先后顺序的,不一定在这个线程的回调函数注册完之后正好这个线程的异常就发生了。那么当程序中出现异常的时候,到底系统会调用哪个回调函数呢。
我们知道在每一个EXCEPTION_REGISTRATION结构里面都存了上一个EXCEPTION_REGISTRATION结构的地址,所以当我们注册了很多异常回调函数之后,这些EXCEPTION_REGISTRATION结构就构成了像一条链子一样的结构。
线程信息块(NT_TIB) EXCEPTION_REGISTRATION 异常处理程序(回调函数)
(图片不清晰,只能这样标注一下)
上图就是SEH链的示意图。中间那块是互相联系的EXCEPTION_REGISTRATION结构。当程序发生异常时,系统按照以下步骤来处理:
- 系统查看发生异常的进程是否正在被调试,如果是就向调试器发送EXCEPTION_DEBUG_EVENT事件。如果系统没有被调试或者调试器拒绝处理这个异常,那么执行步骤2
- 系统检测异常所处的线程,查看这个线程环境,看看它的FS:[0]处是否安装了SEH异常处理回调函数,如果安装了就调用它。
- 线程的回调函数如果可以处理这个异常,则处理完后返回ExceptionContinueExecution字段,这个时候系统就可以结束查找了。
- 如果说线程中SEH异常处理回调函数处理不了这个异常,那么回调函数返回一个ExceptionContinueSearch字段告诉系统它处理不了,去找别人。然后系统就会从EXCEPTION_REGISTRATION结构的prev字段取出上一个结构的地址,然后去调用上一个异常处理回调函数。然后就跳到步骤3。
- 如果顺着这个SEH链一直往前找但是没有一个能够处理异常的话,系统就会再检测一下进程是否在被调试,如果在那就再通知一下调试器。
- 如果没有被调试,或者说这个调试器还是不处理这个异常的话,系统就看一下进程里也没有注册筛选器异常处理回调函数,有的话就调用它。
- 如果链筛选器一异常处理回调函数也没有的话,那系统就只能自己来了,系统会调用默认的异常处理回调函数终止进程。
下面给出一个使用SEH异常处理的例字程序的源代码:
1 .386 2 .model flat,stdcall 3 option casemap:none 4 ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 5 include windows.inc 6 include user32.inc 7 includelib user32.lib 8 include kernel32.inc 9 includelib kernel32.lib 10 ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 11 ;数据段 12 ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 13 .const 14 szMsg db ‘异常发生位置:%08x,异常代码: %08x,标志:%08x‘,0 15 szSafe db ‘回到了安全的地方!‘,0 16 szCaption db ‘SEH例子‘,0 17 18 .code 19 ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 20 ;SEH Handler 异常处理程序 21 ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 22 _Handler proc C _lpExceptionRecord,_lpSEH,_lpContext,_lpDispatcherContext 23 24 local @szBuffer[256]:byte 25 26 pushad 27 mov esi,_lpExceptionRecord 28 mov edi,_lpContext 29 assume esi:ptr EXCEPTION_RECORD,edi:ptr CONTEXT 30 invoke wsprintf,addr @szBuffer,addr szMsg,[edi].regEip,[esi].ExceptionCode,[esi].ExceptionFlags 31 invoke MessageBox,NULL,addr @szBuffer,NULL,MB_OK 32 mov eax,_lpSEH 33 push [eax + 8] 34 pop [edi].regEip 35 push [eax + 0ch] 36 pop [edi].regEbp 37 push eax 38 pop [edi].regEsp 39 assume esi:nothing,edi:nothing 40 popad 41 mov eax,ExceptionContinueExecution 42 ret 43 44 _Handler endp 45 ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 46 start: 47 ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 48 ;在堆栈中构造一个EXCEPTION_REGISTRATION 49 ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 50 assume fs:nothing 51 push ebp 52 push offset _SafePlace 53 push offset _Handler 54 push fs:[0] 55 mov fs:[0],esp 56 ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 57 ;将引发异常的指令 58 ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 59 xor eax,eax 60 mov dword ptr [eax],0 ;这个指令将产生异常 61 62 _SafePlace: 63 invoke MessageBox,NULL,addr szSafe,addr szCaption,MB_OK 64 ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 65 ;恢复原来的SEH链 66 ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 67 pop fs:[0] ;恢复原来的结构地址 68 invoke ExitProcess,NULL 69 ;>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> 70 end start 71 72 73
异常处理回调函数和之前演示的栗子几乎一样,就是多了一个显示信息的窗口。在52行那里特地造成一个内存写入异常,然后程序运行到这就会发生中断,接着就会去调用异常处理回调函数。
当程序发生异常然后执行完回调函数回到安全位置之后,先显示一个窗口显示回到了安全的位置。然后恢复原来的SEH链。
运行程序:
出现这个窗口后点击确定就跳到下面这个窗口。
可以看到第一个窗口显示的信息也没有错,正是我们在程序中故意设置错误的地方。这样就说明这个SEH异常处理回调函数安装成功了。
SEH异常处理还有一个很重要的东西就是:展开操作。但是这个书上没有很详细的讲,我看的也不是很懂。大概意思好像就是如果为一个程序安装了一个SEH异常处理回调函数,然后又为程序里面的一个子程序安装SEH异常处理回调函数。这样在栈里面两个EXCEPTION_REGISTRATION结构会挨得很近,当子程序里面发生异常,但是子程序的回调函数不能处理的的时候,系统会去调用程序的回调函数,这个时候ESP指向一个程序的EXCEPTION_REGISTRATION结构和子程序的EXCEPTION_REGISTRATION结构之间的位置,如果接着程序进行了一些入栈或者出栈的操作,很可能会原来的子程序的EXCEPTION_REGISTRATION结构会被覆盖冲掉,如果再发生一次异常,系统就会调用一个无效的回调函数的地址。
而展开操作,就是将步处理异常的回调函数给卸载了,比如,如果一个程序的回调函数都不处理一个异常,并且最后由系统来处理了这个异常,那么所有的回调函数都会被再调用一次,这时函数要做的就是进行一些扫尾工作,然后返回ExceptionCollidedUnwind字段。当某一个回调函数发起展开操作(也就说明,从第一个回调函数开始,一直到它才处理这个异常,那么那些不处理异常的回调函数全部都要卸载),系统就会以EXCEPTION_UNWIND代码和EXCEPTION_UNWINDING代码去依次调用每个回调函数,这两个代码就是告诉函数你要被卸载了,一直调用到处理异常的那个回调函数。然后将这个回调函数之前的所有回调函数卸载。最后,这个发起展开操作的回调函数就成了SEH链上的第一个回调函数。
这样做就可以避免调用一个无效回调函数的地址的情况。
所以如果要写一个强壮一点的回调函数,可以在回调函数里面加一条分支语句,如果这个错误本函数可以解决,那么就进行展开操作。具体怎么进行展开操作的话,书上就更没仔细讲了。
一种方法是用一个循环以EXCEPTION_UNWINDING标志调用之前的每一个回调函数,完事后再重新设置一下FS:[0]。
另一种方法是调用RtlUnwind函数,这个函数似乎没公开(书上写的,不知道现在公开了没),但是网上应该查得到,有兴趣的可以查一下。
(应该是这样,如果有错误请大佬指出)。
以上是关于使用筛选器和SEH处理异常的主要内容,如果未能解决你的问题,请参考以下文章