最近看罗云彬老师的 《琢石成器》--- 异常处理。转两个帖子,补充下知识。慢慢看
【 以下文字转载自 Virus 讨论区 】
发信人: RonLiang (俩零), 信区: Virus
标 题: SEH in ASM 研究 By Hume
发信站: BBS 水木清华站 (Thu Jun 19 15:24:28 2003), 转信
-=-=-=-=-=>
SEH in ASM 研究
By Hume/冷雨飘心
为什么老调重弹:
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通过seh机制给了应用程序一个机会来修补错误,事实
上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
szCapdb "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:
pushoffset Final_Handler
callSetUnhandledExceptionFilter
;调用SetUnhandledExceptionFilter来安装final SEH
;原型很简单SetUnhandledExceptionFilter
proto
;pTopLevelExceptionFilter:DWORD
xor ecx,ecx
mov eax,200
cdq
divecx ;除0错误
;以下永远不会被执行
invokeMessageBox,0,addr szMsgERR1,addr szCap,30h+1000h
invokeExitProcess,0 ;30h=MB_ICONEXCLAMATION
;1000h=MB_SYSTEMMODAL
;-----------------------------------------
Final_Handler:
invokeMessageBox,0,addr szMsgOK,addr szCap,30h
moveax,EXCEPTION_EXECUTE_HANDLER ;==1 这时不出现非法操作的讨厌对话框
;moveax,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
szCapdb "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
divecx
;以下永远不会被执行
invokeMessageBox,0,addr szMsgERR1,addr szCap,10h+1000h
pop fs:[0] ;清除seh链表
add esp,4
invokeExitProcess,0
;============================
perThread_Handler:
invokeMessageBox,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 ***--> %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:
invokeMessageBox,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
;-----------------------------------------
dispMsgproc ;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
dispMsgendp
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
szCapdb "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
divecx
WouldBeOmit: ;//正常情况以下永远不会被执行
add eax,100 ;//这里不会执行,因为我们改变
了eip的值
ExecuteHere:
div ecx ;//从这里开始执行,从结果
可以看到
invokewsprintf,addr buff,addr fmt,addr szMsg1,eax
invokeMessageBox,NULL,addr buff,addr szCap,40h+1000h
pop fs:[0] ;//修复后显示20,因为我们
让ecx=10
add esp,4
invokeExitProcess,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_xHandler1proto C :DWORD,:DWORD,:DWORD,:DWORD
per_xHandler2proto C :DWORD,:DWORD,:DWORD,:DWORD
per_xHandler3proto 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 ;除法错误
invokeMessageBox,0,ddd("Good,divide overflow was solved!"),addr
sztit,40h
sub eax,eax
mov [eax],ebx ;内存写错误
succ:
invokeMessageBox,0,ddd("Good,memory write violation solved!"),
addr sztit,40h
db 0F0h,0Fh,0C7h,0C8h ;什么cmpchg8b指令的非法形式?我从来没
有成功过!!
;演示程序中使用seh实现SMC技术,加密
??...
invokeMessageBox,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
invokeExitProcess,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:
invokeMessageBox,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:
invokeMessageBox,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
invokeRtlUnwind,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:
invokeMessageBox,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:
invokeMessageBox,0,CTEXT("Note... unwinding in xhandler3..."),
addr sztit,40h
@cantdo3:
popad
MOV EAX,ExceptionContinueSearch
RET
per_xHandler3 ENDP
;-----------------------------------------
;lame routine for debug
Dispcontproc
inc count
call dispMsg
ret
Dispcontendp
dispMsgproc
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 ***--> %8X",0dh,0ah
db "it's the %d times xhandler was called!",0
.code
invokewsprintf,addr szbuf,addr fmt,ecx,edx,eax,ebx,count
invokeMessageBox,0,addr szbuf,CTEXT("related Mess of context"),
0
popad
ret
dispMsgendp
;;------------------------------------------------
END_Start
;------------------下面是我在本文里用到到的宏,我的mac.h比较长,就不贴了
-----
dddMACRO 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
下是可以的,就像在前面例子中用seh实现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_xHandlerproto 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
invokewsprintf,addr szbuf,ddd("It's in ring0,please see
CR3==%08X",0dh,oah,"following display Modified
info..."),ebx
invokeMessageBox,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:
invokeMessageBox,0,ebx,ddd("Rin0 SMC test"),eax
_exit:
;--------------------
pop fs:[0]
add esp,4
invokeExitProcess,0
NT_2K_XP:
invokeMessageBox,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_xHandlerproto 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
invokeMessageBox,0,addr Msg0,ddd("The Eax equal to..."),0
pop fs:[0]
add esp,4
invokeExitProcess,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
后话:
最近很郁闷,生活上的工作上的,就这样懒懒懒散散地目无光彩地苟活于世,最近看
了一点C++,有的地方头大,于是拿起老家伙asm写了点以前许诺过的东西,感觉还是
ASM最有助于理解基本原理~~~不过C/C++给了我们另外的工具,虽然目标代码不够紧
凑,毕竟不需要我们每个人写内核,C/C++可以提高生产能力. crack也一样,如果一
些基本概念都不懂明白还谈什么crack?昨天看精华III一些高手的文章,真是惭愧啊
!
我的专业不是计算机或者以后永远也不会搞计算机,这些当也许只是业余爱好,永远
不会放弃的业余爱好.以后也许很长一段时间里面我会离开大家,毕竟,面临的还有
生活.
由于有关seh的文档资料比较少,所以有了这篇学习心得,在看到这里的时候如果前
面的已经完全理解相信seh对我们而言不会再是什么难题,由于seh是win32通用的错
误处理方法Nt/2K/Xp仍然支持但某些细节或许有所改变,这需要你自己的研究.
写这点笔记的过程也是我自己学习的一个过程,由于时间紧张,乱七八糟还算写完了
感谢你能耐心看到这里,如果有什么错误或者你有什么好的想法,请不要忘了告诉我
.
作任何事情也许都要善始善终,由于前一段时间比较郁闷,曾一度想放弃.感谢
hying,夜月,破解勇,fpc,FiNALSErAPH(这次拼对了?呵呵...),皮皮,dREAMtHEATER
等还有许多其他朋友,没有你们的鼓励和督促,我不会坚持写到最后.
参考资料:
1.Jeffy Ritcher <<windows高级编程指南>> tsinghua press
2.Matt Pietrek 1996 MSJ<<A Crash Course on the Depths of Win32?
Structured Exception Handling>>
3.Jeremy Gordon <<Win32 Exception handling for assembler programmers>>
4.owl和EliCZ等asm高手的汇编源代码
5.others I can’t remember
QQ: 8709369
humewen@21cn.com
humewen@263.net
humeasm.yeah.net
hume.longcity.net
hume/冷雨飘心
2002.1写毕
2002.2 整理
-=-=-=-=-=>
--
当你的电脑被病毒或者木马侵害的时候,请到virus版。
如果你受益于virus版,那么今后就请你帮助你的病友们,因为你深知他们所处的境地。
如果virus版未能如你所愿,那么也请你为了他们,将后来跟病毒斗争的经验告诉他们。
当你对病毒反病毒技术感兴趣的时候,请到virus版。共同提高、共同关心计算机安全。
virus版需要每一位网友的支持!
[招生]科锐逆向工程师培训(2024年11月15日实地,远程教学同时开班, 第51期)