SEH in ASM研究

SEH in ASM研究

作者: Hume

第一部分 基础篇

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可以修复这些异常或者干我们想干的任何事情然后从希望的地方继续执行,嘿嘿,很爽吧,可惜我们没有足够的信息,那里找到我们所需要的信息?

PART II 继续深入

三、传递给异常处理例程的参数

I、传递给final型的参数,只有一个即指向EXCEPTION_POINTERS结构的指针, EXCEPTION_POINTERS定义如下:
EXCEPTION_POINTERS STRUCT
pExceptionRecord DWORD ?
ContextRecord DWORD ?
EXCEPTION_POINTERS ENDS

执行时堆栈结构如下:
esp -> ptEXCEPTION_POINTERS
然后执行call _Final_Handler
注意堆栈中的参数是指向EXCEPTION_POINTERS 的指针,而不是指向pExceptionRecord的指针

以下是EXCEPTION_POINTERS两个成员的详细结构
EXCEPTION_RECORD STRUCT
ExceptionCode DWORD ? ;异常码
ExceptionFlags DWORD ? ;异常标志
PExceptionRecord DWORD ? ;指向另外一个EXCEPTION_RECORD的指针
ExceptionAddress DWORD ? ;异常发生的地址
NumberParameters DWORD ? ;下面ExceptionInformation所含有的dword数目
ExceptionInformation DWORD EXCEPTION_MAXIMUM_PARAMETERS dup(?)
EXCEPTION_RECORD ENDS ;EXCEPTION_MAXIMUM_PARAMETERS ==15

;================具体参数解释========================================
ExceptionCode 异常类型,SDK里面有很多类型,但你最可能遇到的几种类型如下:
C0000005h----读写内存冲突
C0000094h----非法除0
C00000FDh----堆栈溢出或者说越界
80000001h----由Virtual Alloc建立起来的属性页冲突
C0000025h----不可持续异常,程序无法恢复执行,异常处理例程不应处理这个异常
C0000026h----在异常处理过程中系统使用的代码,如果系统从某个例程莫名奇妙的返回,则出现此代码,例如调用RtlUnwind时没有Exception Record参数时产生的异常填入的就是这个代码
80000003h----调试时因代码中int3中断
80000004h----处于被单步调试状态
注:也可以自己定义异常代码,遵循如下规则:
_____________________________________________________________________+

位: 31~30 29~28 27~16 15~0
_____________________________________________________________________+
含义: 严重程度 29位 功能代码 异常代码
0==成功 0==Mcrosoft MICROSOFT定义 用户定义
1==通知 1==客户
2==警告 28位
3==错误 被保留必须为0
ExceptionFlags 异常标志
0----可修复异常
1----不可修复异常
2----正在展开,不要试图修复什么,需要的话,释放必要的资源
pExceptionRecord 如果程序本身导致异常,指向那个异常结构
ExceptionAddress 发生异常的eip地址
ExceptionInformation 附加消息,在调用RaiseException可指定或者在异常号为C0000005h即内存异常时(ExceptionCode=C0000005h) 的含义如下,其他情况下一般没有意义
第一个dword 0==读冲突 1==写冲突
第二个dword 读写冲突地址

;==========CONTEXT具体结构含义================================

CONTEXT STRUCT ; _
ContextFlags DWORD ? ; |--------------- +00
iDr0 DWORD ? ; | +04
iDr1 DWORD ? ; | +08
iDr2 DWORD ? ; >调试寄存器 +0C
iDr3 DWORD ? ; | +10
iDr6 DWORD ? ; | +14
iDr7 DWORD ? ; _| +18
FloatSave FLOATING_SAVE_AREA <> ;浮点寄存器区 +1C~~+88
regGs DWORD ? ;--| +8C
regFs DWORD ? ; |\段寄存器 +90
regEs DWORD ? ; |/ +94
regDs DWORD ? ;--| +98
regEdi DWORD ? ;____________ +9C
regEsi DWORD ? ; | 通用 +A0
regEbx DWORD ? ; | 寄 +A4
regEdx DWORD ? ; | 存 +A8
regEcx DWORD ? ; | 器 +AC
regEax DWORD ? ;_______|___组_ +B0
regEbp DWORD ? ;++++++++++++++++ +B4
regEip DWORD ? ; |控制 +B8
regCs DWORD ? ; |寄存 +BC
regFlag DWORD ? ; |器组 +C0
regEsp DWORD ? ; | +C4
regSs DWORD ? ;+++++++++++++++++ +C8
ExtendedRegisters db MAXIMUM_SUPPORTED_EXTENSION dup(?)
CONTEXT ENDS
以上是两个成员的详细结构,下面给出一个final型的例子,这也是本文所讨论的最后一个final型的例子,以后的例子集中在thread类型上.
;--------------------------------------------
; Ex3,演示final处理句柄的参数获取,加深前面
; 参数传递的介绍理解如果难于理解请先看partIII
; 再回来看这个例子
;--------------------------------------------
.586
.model flat, stdcall
option casemap :none ; case sensitive
include hd.h
include mac.h
;;--------------
.data
sztit db "exceptION MeSs,by hume[AfO]",0
fmt db "Context eip--> %8X ebx--> %8X ",0dh,0ah
db "Flags Ex.c-> %8x flg--> %8X",0
szbuf db 200 dup(0)
;;-----------------------------------------
.CODE
_Start:
assume fs:nothing
push offset _final_xHandler0
call SetUnhandledExceptionFilter
xor ebx,ebx
mov eax,200
cdq
div ebx
invoke MessageBox,0,ddd("Good,divide overflow was solved!"),addr sztit,40h
xor eax,eax
mov [eax],ebx
invoke ExitProcess,0

;-----------------------------------------
_final_xHandler0:
push ebp
mov ebp,esp

mov eax,[ebp+8] ;the pointer to EXCEPTION_POINTERS
mov esi,[eax] ;pointer to _EXCEPTION_RECORD
mov edi,[eax+4] ;pointer to _CONTEXT
test dword ptr[esi+4],1
jnz @_final_cnotdo
test dword ptr[esi+4],6
jnz @_final_unwind

;call dispMsg
cmp dword ptr[esi],0c0000094h
jnz @_final_cnotdo

mov dword ptr [edi+0a4h],10
call dispMsg

mov eax,EXCEPTION_CONTINUE_EXECUTION ;GO ON
jmp @f

@_final_unwind:
invoke MessageBox,0,CTEXT("state:In final unwind..."),addr sztit,0
;好像不论处理不处理异常,都不会被调用,right?
@_final_cnotdo:
mov eax,EXCEPTION_CONTINUE_SEARCH
jmp @f
@@:
mov esp,ebp
pop ebp
ret
;-----------------------------------------
dispMsg proc ;My lame proc to display some message
pushad
mov eax,[esi]
mov ebx,[esi+4]
mov ecx,[edi+0b8h]
mov edx,[edi+0a4h]
invoke wsprintf,addr szbuf,addr fmt,ecx,edx,eax,ebx
invoke MessageBox,0,addr szbuf,CTEXT("related Mess of context"),0
popad
ret
dispMsg endp
END _Start
;;------------------------------------------------

II、 传递给per_thread型异常处理程序的参数,如下:

在堆栈中形成如下结构
esp -> *EXCEPTION_RECORD
esp+4 -> *ERR ;注意这也就是fs:[0]的指向
esp -> *CONTEXT record ;point to registers
esp -> *Param ;呵呵,没有啥意义

然后执行 call _Per_Thread_xHandler

操作系统调用handler的MASM原型是这样
invoke xHANDLER,*EXCEPTION_RECORD,*_EXCEPTION_REGISTRATION,*CONTEXT,*Param
即编译后代码如下:
PUSH *Param ;通常不重要,没有什么意义
push *CONTEXT record ;上面的结构
push *ERR ;the struc above
push *EXCEPTION_RECORD ;see above
CALL HANDLER
ADD ESP,10h

下一部分给出thread类型的具体实例.

PART III 不是终结

我们的目标是分三步走,学会SEH,现在让我们接触最有趣的部分:SEH的应用.seh设计的最初目的就是为了使应用程序运行得更健壮,因此SEH用于除错,避免应用程序和系统的崩溃是最常见的用途.例如:

1.比如你的程序里出现了除0错,那你就可以在你的seh处理程序中将除数改为非零值,per_Thread seh返回0(ExceptionContinueExecution)、final返回-1 (EXCEPTION_CONTINUE_EXECUTION),系统就会根据你的意图用改变过的context加载程序在异常处继续执行,由于被除数已经改变为非零值,你的程序就可以正常仿佛什么也没有发生的继续执行了.

2.seh还可以处理内存读写异常,如果你分配的堆栈空间不够,产生溢出,这时你就可以处理这个异常,再多分配一些空间,然后结果是你的程序照常运行了,就好像什么也没有发生过,这在提高内存运用效率方面很值得借鉴,虽然会降低一些程序的执行效率.另外,在很多加壳或反跟踪软件中,利用vitualAlloc和VitualProtect制造异常来进入异常程序,或仅仅是用,mov [0],XXX来进入异常程序,要比用int3或者int1或
pushf
and [esp],100h
popf
进入要隐蔽得多,如果可以随机引起这些异常的话,效果会更好...当然应用很多了,感兴趣自己去找.话题似乎有点远了,让我们回到最基础的地方.
前面的例子中你可能已经注意到,假如我们改变了Context的内容,(注意啊,context包含了系统运行时各个重要的寄存器),并且返回0(ExceptionContinueExecution-->perThread SEH),或者-1(EXCEPTION_CONTINUE_EXECUTION,final SEH),就表示要系统已现有的context继续执行程序,当然我们的改变被重载了,就像周星驰的月光宝盒改变了历史一样奇妙,程序就会以改变的context内容去执行程序,通过这种手段,我们可以修复程序,使其继续执行.

看下面的例子4.
读之前,先再罗嗦几句,由于前面介绍了seh例程被调用的时候,系统把相关信息已经压入堆栈,所以我们只要在程序里寻址调用就行了,怎么寻址呢???唉....回顾一下call指令执行的基本知识,一般对于近调用,通过[esp+4]即刻找到
*EXCEPTION_RECORD,其余的不用说了吧,如果执行了push ebp;mov ebp,esp的话,就是[ebp+8]指向*EXCEPTION_RECORD,这也是大多数程序用的和我们最常见到的,明白了吗?不明白?我--去--跳--楼.
;________________________________________________________________________
;|EX.4 By hume,2001,to show the basic simple seh function
;|________________________________________________________________________
.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
szMsg1 db "In normal,It would never Get here!",0
fmt db "%s ",0dh,0ah," 除法的商是:%d",0

buff db 200 dup(0)

.code
_start:
Assume FS:NOTHING
push offset perThread_Handler
push fs:[0]
mov fs:[0],esp ;//建立SEH的基本ERR结构,如果不
;//明白,就仔细研究一下吧
xor ecx,ecx
mov eax,200
cdq
div ecx
WouldBeOmit: ;//正常情况以下永远不会被执行
add eax,100 ;//这里不会执行,因为我们改变了eip的值

ExecuteHere:
div ecx ;//从这里开始执行,从结果可以看到
invoke wsprintf,addr buff,addr fmt,addr szMsg1,eax
invoke MessageBox,NULL,addr buff,addr szCap,40h+1000h
pop fs:[0] ;//修复后显示20,因为我们让ecx=10
add esp,4
invoke ExitProcess,NULL


perThread_Handler proc \
uses ebx pExcept:DWORD,pFrame:DWORD,pContext:DWORD,pDispatch:DWORD
mov eax,pContext
Assume eax:ptr CONTEXT
mov [eax].regEcx,20 ;//Ecx改变
lea ebx, ExecuteHere
mov [eax].regEip,ebx ;//从我们想要的地方开始执行,嘿嘿,这就是很多
;//反跟踪软件把你引向的黑暗之域
mov eax,0 ;//ExceptionContinueExecution,表示已经修复
;//CONTEXT,可从异常发生处
;//reload并继续执行
ret
perThread_Handler endp
end _start
;//====================================================
哈哈,从这个例子里我门可以真正看到seh结构化处理的威力,他不仅恢复了ecx的内容而且使程序按照你想要的顺序执行了,哈哈,如果你对反跟踪很感兴趣的话,你还可以在例程中加入
xor ebx,ebx
mov [eax].iDr0,ebx
mov [eax].iDr2,ebx
mov [eax].iDr3,ebx
mov [eax].iDr4,ebx
清除断点,跟踪者....嘿嘿,不说你也体验过,当然也可以通过检验drx的值来判断是否被跟踪,更复杂地,你可以设置dr6,和dr7产生一些有趣的结果,我就不罗嗦了.

上面的例子理解了吧,因为我用的是MASM提供的优势来简化程序,老Tasm Fans可能会不以为然,你可以试一下下面的代码代替,是TASM,MASM compatibale的
perThread_Handler:
push ebp
mov ebp,esp
mov eax,[ebp+10h] ;取context的指针
mov [eax+0ach],20 ;将ecx=0,可以对照前面的例程和context结构
lea ebx, ExecuteHere
mov [eax+0b8h],ebx ;eip== offset ExecuteHere,呵呵
xor eax,eax
mov esp,ebp
pop ebp
ret
这是raw asm的,不过masm既然给我们设计了这么多好东西,我们为什么不好好利用呢?

好,到现在为止,基本知识已经结束了,我们应该可理解seh的相关文章和写简单的seh处理程序了,但关于seh还只是刚刚开始,很多内容和应用还没有涉及到,请继续看提高篇.

第二部分提高篇

PART IV 关于异常处理的嵌套和堆栈展开

在实际程序设计过程中,不可能只有一个异常处理例程,这就产生了异常处理程序嵌套的问题,可能很多处理例程分别监视若干子程序并处理其中某种异常,另外一个监视所有子程序可能产生的共性异常,这作起来实际很容易,也方便调试.你只要依次建立异常处理框架就可以了.关于VC++异常处理可以嵌套很多人可能比较熟悉,用起来更容易不过实现比这里也就复杂得多,在VC++中一个程序所有异常只指向一个相同的处理句例程,然后在这个处理例程里再实现对各个子异常处理例程的调用,他的大致方法是建立一个子异常处理例程入口的数组表,然后根据指针来调用子处理例程,过程比较烦琐,原来打算大致写一点,现在发现自己对C/C++了解实在太少,各位有兴趣还是自己参考MSDN Matt Pietrek 1996年写的一篇文章<<A Crash Course on the Depths of Win32? Structured Exception Handling>>,里面有非常详细的说明,对于系统的实现细节也有所讨论,不过相信很多人都没有兴趣.hmmm...:)实际上Kernel的异常处理过程和VC++的很相似.我们的程序中当然也可以采用这种方法,不过一般应用中不必牛刀砍蚂蚁.

有异常嵌套就涉及到我们可能以前经常看到并且被一些污七八糟的不负责任的”翻译家”搞得头晕脑涨不知为何的”异常展开”的问题,也许你注意到了如果按照我前面的例子包括final都不处理异常的话,最后系统在终结程序之前会来一次展开,在试验之后发现,展开不会调用final只是对per_thread例程展开(right?).什么是堆栈展开?为什么要进行堆栈展开?如何进行堆栈展开?

我曾经为堆栈展开迷惑过,原因是各种资料的描述很不一致,Matt Pietrek说展开后前面的ERR结构被释放,并且好像seh链上后面的处理例程如果决定处理异常必须对前面的例程来一次展开,很多C/C++讲述异常处理的书也如斯说这使人很迷惑,我们再来看看Jeremy Gordon的描述,堆栈展开是处理异常的例程自愿进行的.呵呵,究竟事实如何?
在迷惑好久之后我终于找到了答案:Matt Pietrek讲的没有错,那是VC++以及系统kernel的处理方法,Jeremy Gordon说的也是正确的,那是我门asm Fans的自由!

好了,现在来说堆栈展开,堆栈展开这个词似乎会使人有所误解,堆栈怎么展开呢?事实上,堆栈展开是异常处理例程在决定处理某个异常的时候给前面不处理这个异常的处理例程的一个清洗的机会,前面拒绝处理这个异常的例程可以释放必要的句柄对象或者释放堆栈或者干点别的工作...那完全是你的自由,叫stack unwind似乎有点牵强.堆栈展开有一个重要的标志就是EXCEPTION_RECORD.ExceptionFlag为2,表示正在展开,你可以进行相应的处理工作,但实际上经常用的是6这是因为还有一个UNWIND_EXIT equ 4什么的,具体含义未知,不过kernel确实就是检测这个值,因此我们也就检测这个值来判断展开.

注意在自己的异常处理例程中,unwind不是自动的,必须你自己自觉地引发,如果所有例程都不处理系统最后的展开是注定的. 当然如果没有必要你也可以选择不展开.
win32提供了一个api RtlUnwind来引发展开,如果你想展开一下,就调用这个api吧,少候讲述自己代码如何展开

RtlUnwind调用描述如下:
PUSH Return value ;返回值,一般不用
PUSH pExceptionRecord ;指向EXCEPTION_RECORD的指针
PUSH OFFSET CodeLabel ;展开后从哪里执行
PUSH LastStackFrame ;展开到哪个处理例程终止返回,通常是处理异常的Err结构
CALL RtlUnwind

调用这个api之前要注意保护ebx,esi和edi,否则...嘿嘿

MASM格式如下:
Invoke
RtlUnwind,pFrame,OFFSET return_code_Address,pExceptionRecord,Return_value

这样在展开的时候,就以pExceptionRecord.flag=2 依次调用前面的异常处理例程,到决定异常的处理例程停止,Jeremy Gordon手动展开代码和我下面的例子有所不同.他描述最后决定处理异常的ERR结构的prev成员为-1,好像
我的结果和他的有所差异,因此采用了另外的方法,具体看下面的例子.

最后一点要注意在嵌套异常处理程序的时候要注意保存寄存器,否则你经常会得到系统异常代码为C00000027h的异常调用,原因是异常处理中又产生异常,而这个异常又无法解决,只好由操作系统终结你的程序.

一下给出一点垃圾代码演示可能有助于理解,注意link的时候要加入 /section:.text,RWE 否则例子里面的代码段不能写,SMC功能会产生异常以致整个程序不能进行.

注意:2K/XP下非法指令异常的代码不一致,另外用下面的例子在2K下不能工作,具体错误未知.因此只能在9X下演示,为了在2k/Xp下也能运行我加了点代码,有兴趣看看,另外帮我解决一下2K/Xp下SMC的问题?thx!

下面例子很烂,不过MASM格式写起来容易一点,也便于理解.看来比较长实际很简单,耐心看下去.
;-----------------------------------------
;Ex5,演示堆栈展开和异常嵌套处理 by Hume,2002
;humewen@21cn.com
;hume.longcity.net
;-----------------------------------------
.586
.model flat, stdcall
option casemap :none ; case sensitive
include hd.h
include mac.h

;;--------------
per_xHandler1 proto C :DWORD,:DWORD,:DWORD,:DWORD
per_xHandler2 proto C :DWORD,:DWORD,:DWORD,:DWORD
per_xHandler3 proto C :DWORD,:DWORD,:DWORD,:DWORD
;-----------------------------------------

.data
sztit db "except Mess,by hume[AfO]",0
count dd 0,0
Expt1_frm dd 0 ;ERR结构指针,用于堆栈展开手动代码
Expt2_frm dd 0
Expt3_frm dd 0

;;-----------------------------------------
.CODE
_Start:
assume fs:nothing
push offset per_xHandler3
push fs:[0]
mov fs:[0],esp
mov Expt3_frm,esp

push offset per_xHandler2
push fs:[0]
mov fs:[0],esp
mov Expt2_frm,esp

push offset per_xHandler1
push fs:[0]
mov fs:[0],esp
mov Expt1_frm,esp
;--------------------------
;install xhnadler
;-----------------------------------------

xor ebx,ebx
mov eax,200
cdq
div ebx ;除法错误

invoke MessageBox,0,ddd("Good,divide overflow was solved!"),addr sztit,40h

sub eax,eax
mov [eax],ebx ;内存写错误

succ:
invoke MessageBox,0,ddd("Good,memory write violation solved!"),addr sztit,40h

db 0F0h,0Fh,0C7h,0C8h ;什么cmpchg8b指令的非法形式?我从来没有成功过!!
;演示程序中使用seh实现SMC技术,加密??...
invoke MessageBox,0,ddd("illeagal instruction was solved!"),addr sztit,20h
;--------------------------
;uninstall xhnadler
;-----------------------------------------

pop fs:[0]
add esp,4
pop fs:[0]
add esp,4
;或者add esp,10h

pop fs:[0]
add esp,4

invoke ExitProcess,0
;-----------------------------------------
;异常处理句柄1,处理除法异常错误
per_xHandler1 PROC C pExcept:DWORD,pFrame:DWORD,pContext:DWORD,pDispatch:DWORD
pushad
MOV ESI,pExcept
ASSUME ESI:PTR EXCEPTION_RECORD
TEST [ESI].ExceptionFlags,1
JNZ @cantdo1
TEST [ESI].ExceptionFlags,6
JNZ @unwind1
CMP [ESI].ExceptionCode,0C0000094h
JNZ @cantdo1
MOV EDI,pContext

ASSUME EDI:PTR CONTEXT
m2m [edi].regEbx,20 ;将ebx置20,修复除法错误,继续执行
popad
MOV EAX, ExceptionContinueExecution
RET

@unwind1:
invoke MessageBox,0,CTEXT("state: unwinding in xhandler1..."),addr sztit,0
@cantdo1:
popad
MOV EAX,ExceptionContinueSearch
RET
per_xHandler1 ENDP
;-----------------------------------------
;异常处理句柄2,处理内存写错误,扩展可以有其他的例子如自动扩充堆栈
per_xHandler2 PROC C pExcept:DWORD,pFrame:DWORD,pContext:DWORD,pDispatch:DWORD

pushad
MOV ESI,pExcept
ASSUME ESI:PTR EXCEPTION_RECORD
MOV EDI,pContext
ASSUME EDI:PTR CONTEXT

call Dispcont ;显示一点lame的消息,自己调试用

TEST [ESI].ExceptionFlags,1
JNZ @cantdo2
TEST [ESI].ExceptionFlags,6
JNZ @unwind2
CMP [ESI].ExceptionCode,0C0000005h
JNZ @cantdo2
.data ;ASM的数据定义灵活性,如果需要这是可以的
validAddress dd 0
.code

m2m [EDI].regEax,<offset validAddress> ;置eax为有效地址
popad
MOV EAX, ExceptionContinueExecution
RET

@unwind2:
invoke MessageBox,0,CTEXT("hmmm... unwinding in xhandler2..."),addr sztit,40h
@cantdo2:
popad
MOV EAX,ExceptionContinueSearch
RET
per_xHandler2 ENDP
;-----------------------------------------

per_xHandler3 PROC C pExcept:DWORD,pFrame:DWORD,pContext:DWORD,pDispatch:DWORD
pushad
MOV ESI,pExcept
ASSUME ESI:PTR EXCEPTION_RECORD
MOV EDI,pContext
ASSUME EDI:PTR CONTEXT

TEST [ESI].ExceptionFlags,1
JNZ @cantdo3
TEST [ESI].ExceptionFlags,6
JNZ @unwind3
;-----------------------------------------

push ecx
mov ecx,cs
xor cl,cl
jecxz win2k_Xp
win9X:
pop ecx
CMP [ESI].ExceptionCode,0C000001DH ;非法指令异常,与2K/XP下的不一致
JNZ @cantdo3
jmp ok_here
win2k_Xp:
pop ecx ;注意,只有在9X下才可以
CMP [ESI].ExceptionCode,0C000001EH ;非法指令异常->2K/XP
JNZ @cantdo3 ;sMc不成

mov [edi].regEip,offset safereturn
popad
mov eax,0
ret

push ebx
push esi
push edi
comment $ 调用RtlUnwind展开堆栈
lea ebx,unwindback
invoke RtlUnwind,Expt3_frm,ebx,esi,0
$
mov dword ptr [esi+4],2 ;置展开标志,准备展开,这里是
;手动代码
mov ebx,fs:[0]

selfun:
;mov eax,Expt2_frm ;这里显示了ASM手动展开的灵活性
mov eax,Expt3_frm
cmp ebx,eax ;按照Jeremy Gordon的好像不大对头
;cmp dword ptr [ebx],-1 ;这样好像有问题,只好如上,请教答案
jz unwindback
push ebx
push esi ; 压入Err和Exeption_registration结构
call dword ptr[ebx+4]
add esp,8
mov ebx,[ebx]
jmp selfun

unwindback:
invoke MessageBox,0,CTEXT("I am Back!"),addr sztit,40h
pop edi
pop esi
pop ebx ;一定要保存这三个寄存器!

MOV EAX,[EDI].regEip
MOV DWORD PTR[EAX],90909090H ;改为nop指令...SMC? 很简单吧
;SMC注意连接/section:RWE
popad
MOV EAX, ExceptionContinueExecution
RET

@unwind3:
invoke MessageBox,0,CTEXT("Note... unwinding in xhandler3..."),addr sztit,40h
@cantdo3:
popad
MOV EAX,ExceptionContinueSearch
RET
per_xHandler3 ENDP
;-----------------------------------------
;lame routine for debug
Dispcont proc
inc count
call dispMsg
ret
Dispcont endp

dispMsg proc
local szbuf[200]:byte
pushad
mov eax,dword ptr[esi]
mov ebx,dword ptr[esi+4]
mov ecx,dword ptr[edi+0b8h]
mov edx,dword ptr[edi+0a4h]
.data
fmt db "Context eip--> %8X ebx--> %8X ",0dh,0ah
db "Flags Ex.c-> %8x flg--> %8X",0dh,0ah
db "it's the %d times xhandler was called!",0
.code
invoke wsprintf,addr szbuf,addr fmt,ecx,edx,eax,ebx,count
invoke MessageBox,0,addr szbuf,CTEXT("related Mess of context"),0
popad
ret
dispMsg endp

;;------------------------------------------------
END _Start
;------------------下面是我在本文里用到到的宏,我的mac.h比较长,就不贴了-----
ddd MACRO Text ;define data in .data section
local name ;This and other can be used as: ddd("My god!")
.data ;isn't cool?
name db Text,0
.code
EXITM <addr name>
ENDM

CTEXT MACRO y:VARARG ;This is a good macro
LOCAL sym
CONST segment
IFIDNI <y>,<>
sym db 0
ELSE
sym db y,0
ENDIF
CONST ends
EXITM <OFFSET sym>
ENDM

m2m MACRO M1, M2 ;mov is too boring sometimes!
push M2
pop M1
ENDM
;-----------------------------------------
BTW:够长了吧,基本内容介绍完毕,更多内容下一部分介绍一点利用Seh的tricks,哪位大侠有什么好的想法或者有什么错误,请不吝指正,毕竟我是菜鸟...

PART V 利用SEH进入ring0以及单步自跟踪的实现
--SEH的简单应用

实在太累了,这将是最后一部分.

一、ring0!并不遥远...

作为seh的一个有趣的应用是进入ring0,ring0意味着更多的权利,意味着你可以进行一些其他ring3级应用程序不能进行的操作,譬如改自己的代码段(在不修改段属性的前提下),改系统数据(病毒?)等等,在9X下进入ring0的方法很多,在NT下困难的多,SEH只是其中较简单的一种.打开调试器看看系统kernel的工作状态,在9X下cs一般是28h,ds,ss等通常是30h,因此只要我们的cs和ss等在异常处理程序中被赋予上述ring0选择子值,进入ring0就可以实现.可能我们需要执行较复杂的操作,在ring0下一般不能直接调用常用api,当然VxD,WDM等提供的系统服务是另外一种选择. 否则,这在用下述简单方法进入ring0后执行会产生错误,因此,我们在ring0下尽快完成需要完成的任务,然后迅速返回ring3.
在ring0下要完成如下任务:

1.取CR3的值,返回ring3显示.在ring3下不可以读取cr3的值.你可以打开kernel调试器看看例子程序取到的值是否正确.
2.修改代码段后面的jmp ****代码,这在通常情况下只会导致保护错误.而在ring0下是可以的,就像在前面例子中用she实现SMC的效果是一样的,最后显示几个MsgBox,证明我们曾经到达过ring0

这个例子是参考owl的那个nasm写的例子用masm改写,并增加ring0下SMC的代码部分以作演示.另外代码中iretd指令并不是简单实现跳转,而是实现从ring0切回ring3的功能,在变换代码特权级的同时,堆栈的也要变换到ring3.可能原例子ljtt前辈的中文注释容易引起初学者的误解.

别的不说,我发现进入ring0后修改代码段可以使trw的跟踪崩溃...hmmm,好消息?代码如下:
其中用的一些宏在Ex5中已经贴了,就不再重复.
;-----------------------------------------
;Ex6,演示利用seh进入ring0! by Hume,2002
;humewen@21cn.com
;hume.longcity.net
;-----------------------------------------
.586
.model flat, stdcall
option casemap :none ; case sensitive
include hd.h
include mac.h

;;--------------
ring0_xHandler proto C :DWORD,:DWORD,:DWORD,:DWORD
.data
szbuf db 100 dup (0)
count dd 0
;;-----------------------------------------
.CODE
_Start:
assume fs:nothing
push offset ring0_xHandler
push fs:[0]
mov fs:[0],esp
;--------------------
mov ecx,ds
test ecx,100b
jz NT_2K_XP ;NT/2K/XP has no LDT
pushfd
mov eax,esp

int 3
mov ebx,cr3 ;现在,正式宣布,进入ring0!
;呵呵这样简单就进入ring0了,至于进入
push ebx ;ring0有啥用,不要问我!
lea ebx,offset _modi ;SMC
mov byte ptr[ebx],75h ;修改jmp addinfo为jnz addinfo指令
pop ebx

push edx ;ss
push eax ;esp
push dword ptr[eax] ;eflags
push ecx ;cs
push offset ring3back ;eip
iretd ;这里是通过iretd 指令返回特权级3

ring3back:
popfd
invoke wsprintf,addr szbuf,ddd("It's in ring0,please see CR3==%08X",0dh,oah,"following display Modified info..."),ebx
invoke MessageBox,0,addr szbuf,ddd("Ring0! by Hume[AfO]"),40h
xor eax,eax
;add eax,2
.data
Nosmc db "Not modified area!",0
besmc db "haha,I am modified by self in ring0!",0
.code
mov ebx,offset Nosmc
mov eax,0
_modi:
jmp addinfo ;SMC后将这里改为jnz addinfo

mov ebx,offset besmc
mov eax,30h
addinfo:
invoke MessageBox,0,ebx,ddd("Rin0 SMC test"),eax
_exit:
;--------------------
pop fs:[0]
add esp,4
invoke ExitProcess,0

NT_2K_XP:
invoke MessageBox,0,ddd("The example not support NT/2K/Xp,only 9x!"),ddd("By hume"),20h
jmp _exit
;-----------------------------------------
ring0_xHandler PROC C pExcept:DWORD,pFrame:DWORD,pContext:DWORD,pDispatch:DWORD
pushad
assume edi:ptr CONTEXT
assume esi:ptr EXCEPTION_RECORD

mov esi,pExcept
mov edi,pContext
test dword ptr[esi+4],1 ;Exception flags
jnz @f
test dword ptr[esi+4],6
jnz @f
cmp dword ptr[esi],80000003h ;break ponit flag
jnz @f

m2m [edi].regEcx,[edi].regCs ;保存3级代码段选择子
mov [edi].regCs,28h ;0级代码段选择子

m2m [edi].regEdx,[edi].regSs ;保存3级堆栈段选择子
mov [edi].regSs,30h ;0级堆栈选择子

mov dword ptr[esp+7*4],0
popad
ret
@@:
mov dword ptr[esp+7*4],1
popad
ret
ring0_xHandler ENDP
;-----------------------------------------
END _Start

由于在NT/2K/XP下这种进入ring0的方法不能使用,所以首先区别系统版本,如果是NT/2K/XP则拒绝执行, 原理是在NT/2K/XP下没有LDT,因此测试选择子是否指向LDT,这是一种简单的方法,但不推荐使用, 最好使用GetVersionEx...至于
mov dword ptr[esp+7*4],0
popad
是返回eax=1的实现.

二.seh实现单步自跟踪.

有时如果你对SICE,TRW或者其他调试器显示的信息有所怀疑的话,你可以用seh显示一些信息作为简单的调试手段,当然,单步跟踪的用途远不止于此.首先回忆一下我们以前了解的单步的概念,当EFLAGS的TF位为1的话执行完某条指令后CPU将产生单步异常,与执行软指令int1类似.注意产生单步陷阱后eip已经指向下一条指令.但进入单步异常处理程序后cpu自动清除TF,以便下条指令正常执行.

我们要作的就是在seh例程中继续置TF位为1,以便下一条指令执行完毕后继续产生单步陷阱实现跟踪功能,直到遇到popfd指令为止,当然你也可以随便检测其他指令或者用记数器来终止单步.

下面例子中如果没有单步跟踪eax的最后结果是3,由于有了单步自跟踪,在seh处理例程中我们每中断一次要加1,所以最后的结果是7,呵呵.请看下面的例子.
;-----------------------------------------
;Ex7,演示利用seh单步自跟踪 by Hume,2002
;humewen@21cn.com
;hume.longcity.net
;-----------------------------------------
.586
.model flat, stdcall
option casemap :none ; case sensitive
include hd.h
include mac.h

singlestep_xHandler proto C :DWORD,:DWORD,:DWORD,:DWORD
;;--------------
.data
count dd 0
Msg0 db "Eax=="
DispEAX dd 0,0
;;-----------------------------------------
.CODE
_Start:
assume fs:nothing
push offset singlestep_xHandler
push fs:[0]
mov fs:[0],esp
;------------------
xor eax,eax
pushfd
pushfd
or dword ptr[esp],100h
popfd ;置TF标志进入单步状态

nop ; nop执行完后单步异常引发
inc eax ; eip指向,nop后面的指令,就是这里
inc eax ; 单步执行
inc eax ; normal eax==3,but infact eax==7

popfd
;------------------
add eax,30h ;convert to ASCIIZ
mov DispEAX,eax
invoke MessageBox,0,addr Msg0,ddd("The Eax equal to..."),0

pop fs:[0]
add esp,4
invoke ExitProcess,0
;-----------------------------------------
singlestep_xHandler PROC C pExcept:DWORD,pFrame:DWORD,pContext:DWORD,pDispatch:DWORD
pushad
assume edi:ptr CONTEXT
assume esi:ptr EXCEPTION_RECORD

mov esi,pExcept
mov edi,pContext

test dword ptr[esi+4],1 ;Exception flags test common stuff
jnz @f
test dword ptr[esi+4],6
jnz @f
cmp dword ptr[esi],80000004h ;是否为单步异常标志
jnz @f

inc [edi].regEax
mov ebx,[edi].regEip
cmp byte ptr[ebx],9Dh ;是否是popfd,因为目的是取消
jz @finish_singlestep ;单步状态,所以这时就不应该重置TF
or [edi].regFlag,100h ;否则,重置TF
;每单步中断一次,eax加1
;所以eax最后不等于3,而是
;中断4次后,eax==7
@finish_singlestep:

mov dword ptr[esp+7*4],0 ;eax==0 hehe...
popad ;研究一下pushad指令就明白了
ret
@@:
mov dword ptr[esp+7*4],1 ;eax==1
popad
ret

singlestep_xHandler ENDP
;-----------------------------------------
END _Start

由于有关seh的文档资料比较少,所以有了这篇学习心得,在看到这里的时候如果前面的已经完全理解相信seh对我们而言不会再是什么难题,由于seh是win32通用的错误处理方法Nt/2K/Xp仍然支持但某些细节或许有所改变,这需要你自己的研究.

转载于:https://www.cnblogs.com/F4ncy/archive/2005/03/30/128881.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值