第二部分提高篇
By Hume[AfO]/冷雨飘心
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格式写起来容易一点,也便于理解.看来比较长实际很简单,耐心看下去.
;-----------------------------------------
;代码部分删除,腾讯不给发
;-----------------------------------------
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
参考资料:
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
humewen@21cn.com
humewen@263.net
humeasm.yeah.net
[the way of Hume]
2002.1写毕
2002.2 整理