为什么老调重弹:
SEH出现已绝非一日,但很多人可能还不彻底了解Seh的运行机制;有关seh的知识资料不是很多,asm级的详细资料就更少!seh不仅可以简化程序错误处理,使你的程序更加健壮,还被广泛应用于反跟踪以及加解密中,因此,了解seh非常必要,但遗憾的是关于seh详细介绍的中文资料非常少,在实践的基础上,把自己学习的一点笔记奉献给大家,希望对喜欢ASM的朋友有所帮助.如有错误,请高手不吝指正.
第一部分 基础篇
PART I 简单接触
一、SEH背景知识
SEH("Structured Exception Handling"),即结构化异常处理.是(windows)操作系统提供给程序设计者的强有力的处理程序错误或异常的武器.在VISUAL C++中你或许已经熟悉了_try{} _finally{} 和_try{} _except {} 结构,这些并不是编译程序本身所固有的,本质上只不过是对windows内在提供的结构化异常处理的包装,不用这些高级语言编译器所提供的包装 ,照样可以利用系统提供的强大seh处理功能,在后面你将可以看到,用系统本身提供seh结构和规则以及ASM语言,我们将对SEH的机制以及实现进行一番(深入?)探究.
使用windows的人对microsoft设计的非法操作对话框一定不会陌生,尤其是在9X下.这表示发生了一个错误,如果是应用程序的错误,那么windows可能要关闭应用程序,如果是系统错误,你很可能不得不reset以重新启动计算机.从程序编写的角度来看,这种异常产生的原因很多,诸如堆栈溢出,非法指令,对windows保护内存的读写权限不够等等.幸运的是windows通过she机制给了应用程序一个机会来修补错误,事实上windows内部也广泛采用seh来除错.让我们先来看看如果一个应用程序发生错误后windows是怎么处理的.
程序发生异常时系统的处理顺序(most by Jeremy Gordon):
1.因为有很多种异常,系统首先判断异常是否应发送给目标程序的异常处理例程,如果决定应该发送,并且目标程序正处于被调试状态,则系统挂起程序并向调试器发送EXCEPTION_DEBUG_EVENT消息.剩下的事情就由调试器全权负责.如果系统级调试器存在,对于int 1,int 3这样的异常在faults on时一般是会选择处理的,因而如果你的异常处理程序由他们来进入,则不会得到执行,呵呵,这不是正好可以用来探测调试器的存在吗?
2.如果你的程序没有被调试或者调试器未能处理异常(/错误),系统就会继续查找你是否安装了线程相关的异常处理例程,如果你安装了线程相关的异常处理例程,系统就把异常发送给你程序的线程相关的seh处理例程,交由其处理.
3.每个线程相关的异常处理例程可以处理或者不处理这个异常,如果他不处理并且安装了多个线程相关的异常处理例程,可交由链起来的其他例程处理.
4.如果这些例程均选择不处理异常,如果程序处于被调试状态,操作系统仍会再次挂起程序通知debugger.
5.如果程序未处于被调试状态或者debugger没有能够处理,并且你调用SetUnhandledExceptionFilter安装了final型异常处理例程的话,系统转向对它的调用.
6.如果你没有安装最后异常处理例程或者他没有处理这个异常,系统会调用默认的系统处理程序,通常显示一个对话框, 你可以选择关闭或者最后将其附加到调试器上的调试按钮.如果没有调试器能被附加于其上或者调试器也处理不了,系统就调用ExitProcess终结程序.
7.不过在终结之前,系统仍然对发生异常的线程异常处理句柄来一次展开,这是线程异常处理例程最后清理的机会.
以上大致描述了异常/错误发生时系统的逻辑处理顺序,如果你看了一头雾水的话,别着急,化点时间慢慢理解或者进入下一部分实例操作.作几个例子后你也许就会慢慢理解了.
你要有一个最基本的观念就是She只不过是系统在终结你应用程序之前给你的一个最后处理错误的机会,从程序设计的角度来说就是给你自己设计的一个回调函数执行的机会.
二.初步实战演习:
你在自己程序里可以设计两种异常处理例程,一种是通过SetUnhandledExceptionFilter API设置的,姑且称之为final型的,他是进程相关的,也就是说在线程相关的异常处理部分不能处理的异常才会到达final处理例程.
另外一种是线程相关的,他一般用来监视处理进程中某个线程的异常情况.他比较灵活,可选择监视线程中的某一小段代码.姑且称之为thread型的.
下面看看如何设计一个最简单的异常处理程序.
挂接异常处理例程:
I. final型的异常处理
对于final型的,在你的异常未能得到调试器以及线程相关处理例程处理操作系统在即将关闭程序之前会回调的例程,这个例程是进程相关的而不是线程相关的,因此无论是哪个线程发生异常未能被处理,都会调用这个例程.
见下面的例子1:
;==========================================
; ex. 1,by Hume,2001 演示final型异常处理
;==========================================
.586p
.model flat, stdcall
option casemap :none ; case sensitive
include c:/hd/hd.h ;头部包含文件
include c:/hd/mac.h ;常用宏,自己维护一个吧
;;--------------
.data
szCap db "By Hume[AfO],2001...",0
szMsgOK db "OK,the exceptoin was handled by final handler!",0
szMsgERR1 db "It would never Get here!",0
.code
_start:
push offset Final_Handler
call SetUnhandledExceptionFilter
;调用SetUnhandledExceptionFilter来安装final SEH
;原型很简单SetUnhandledExceptionFilter proto
;pTopLevelExceptionFilter:DWORD
xor ecx,ecx
mov eax,200
cdq
div ecx ;除0错误
;以下永远不会被执行
invoke MessageBox,0,addr szMsgERR1,addr szCap,30h+1000h
invoke ExitProcess,0 ;30h=MB_ICONEXCLAMATION
;1000h=MB_SYSTEMMODAL
;-----------------------------------------
Final_Handler:
invoke MessageBox,0,addr szMsgOK,addr szCap,30h
mov eax,EXCEPTION_EXECUTE_HANDLER
;==1 这时不出现非法操作的讨厌对话框
;mov eax,EXCEPTION_CONTINUE_SEARCH
;==0 出现,这时是调用系统默认的异常
;处理过程,程序被终结了
;mov eax,EXCEPTION_CONTINUE_EXECUTION
;==-1 不断出现对话框,你将陷入死循环,可
;别怪我
ret 4
end _start
简单来解释几句,windows根据你的异常处理程序的返回值来决定如何进一步处理
EXCEPTION_EXECUTE_HANDLER equ 1 表示我已经处理了异常,可以优雅地结束了
EXCEPTION_CONTINUE_SEARCH equ 0 表示我不处理,其他人来吧,于是windows调用默认的处理程序,显示一个除错对话框,并结束
EXCEPTION_CONTINUE_EXECUTION equ -1 表示错误已经被修复,请从 异常发生处继续执行
你可以试着让程序返回0和-1然后编译程序,就会理解我所有苍白无力的语言...
II.线程相关的异常处理.
通常每个线程初始化准备好运行时fs指向一个TIB结构(THREAD INFORMATION BLOCK),这个结构的第一个元素fs:[0]指向一个_EXCEPTION_REGISTRATION结构,具体结构见下,后面_EXCEPTION_REGISTRATION为了简化,用ERR来代替这个结构...不要说没见过啊...
fs:[0]->
_EXCEPTION_REGISTRATION struc
prev dd ? ;前一个_EXCEPTION_REGISTRATION结构
handler dd ? ;异常处理例程入口....呵呵,现在明白该怎么作了吧
_EXCEPTION_REGISTRATION ends
我们可以建立一个ERR结构然后将fs:[0]换成指向他的指针,当然最常用的是堆栈,如果你非要用静态内存区也可以,
把handler域换成你的程序入口,就可以在发生异常时调用你的代码了,好马上实践一下,见例子2
;===========================================
; ex. 2,by Hume,2001 线程相关的异常处理
;===========================================
.386
.model flat, stdcall
option casemap :none ; case sensitive
include hd.h
;;============================
.data
szCap db "By Hume[AfO],2001...",0
szMsgOK db "It's now in the Per_Thread handler!",0
szMsgERR1 db "It would never Get here!",0
.code
_start:
ASSUME FS:NOTHING ;否则Masm编译报错
push offset perThread_Handler
push fs:[0]
mov fs:[0],esp ;建立SEH的基本ERR结构,如果
;不明白,就仔细研究一下前面所述
xor ecx,ecx
mov eax,200
cdq
div ecx
;以下永远不会被执行
invoke MessageBox,0,addr szMsgERR1,addr szCap,10h+1000h
pop fs:[0] ;清除seh链表
add esp,4
invoke ExitProcess,0
;============================
perThread_Handler:
invoke MessageBox,NULL,addr szMsgOK,addr szCap, 40h+1000h
mov eax,1 ;ExceptionContinueSearch,不处理,由其他例程或系统处理
;mov eax,0 ;ExceptionContinueExecution,表示已经修复CONTEXT,可
;从异常发生处继续执行
ret ;这里如果返回0,你会陷入死循环,不断跳出对话框....
end _start
嘿嘿,这个简单吧,我们由于没有足够的资料,暂时还不能修复ecx的值使之从异常发生处继续执行,只是简单显示一个MSG,然后让系统处理,自然跳出讨厌的对话框了....
注意化5分钟研究和final返回值的含义不同...windows也是根据返回值来决定下一步的动作的.
好像到此为止,我们并没有从异常处理中得到任何好处,除了在异常发生后可以执行一点我们微不足道的代码,事实上SEH可以修复这些异常或者干我们想干的任何事情然后从希望的地方继续执行,嘿嘿,很爽吧,可惜我们没有足够的信息,那里找到我们所需要的信息?