Chinaunix首页 | 论坛 | 博客
  • 博客访问: 1411874
  • 博文数量: 416
  • 博客积分: 13005
  • 博客等级: 上将
  • 技术积分: 3297
  • 用 户 组: 普通用户
  • 注册时间: 2006-04-05 16:26
文章分类

全部博文(416)

文章存档

2014年(1)

2013年(4)

2012年(46)

2011年(64)

2010年(12)

2009年(4)

2008年(40)

2007年(187)

2006年(58)

分类: WINDOWS

2007-10-12 11:49:34

SEH出现已绝非一日,但很多人可能还不彻底了解Seh的运行机制;有关seh的知识资料不是很多,asm级的详细资料就更少!seh不仅可以简化程序错误处理,使你的程序更加健壮,还被广泛应用于反跟踪以及加解密中,因此,了解seh非常必要, 但遗憾的是关于seh详细介绍的中文资料非常少,在实践的基础上,把自己学习的一点笔记奉献给大家,希望对喜欢ASM的朋友有所帮助.如有错误,请高手不吝指正.

  一、SEH背景知识

  SEH("Structured Exception Handling"),即结构化异常处理.是操作系统提供给程序设计者的强有力的处理程序错误或异常的武器.在VISUAL C++中你或许已经熟悉了_try{} _finally{} 和_try{} _except {} 结构,这些并不是编译程序本身所固有的,本质上只不过是对windows内在提供的结构化异常处理的包装,不用这些高级语言编译器所提供的包装 ,照样可以利用系统提供的强大seh处理功能,在后面你将可以看到,用系统本身提供seh结构和规则以及ASM语言, 我们将对SEH的机制以及实现有一个彻底的了解.

  发生异常时系统的处理顺序(by Jeremy Gordon):

  1.系统首先判断异常是否应发送给目标程序的异常处理例程,如果决定应该发送,并且目标程序正在被调试,则系统挂起程序并向调试器发送EXCEPTION_DEBUG_EVENT消息.呵呵,这不是正好可以用来探测调试器的存在吗?

  2.如果你的程序没有被调试或者调试器未能处理异常,系统就会继续查找你是否安装了线程相关的异常处理例程,如果你安装了线程相关的异常处理例程,系统就把异常发送给你的程序seh处理例程,交由其处理.

  3.每个线程相关的异常处理例程可以处理或者不处理这个异常,如果他不处理并且安装了多个线程相关的异常处理例程, 可交由链起来的其他例程处理.

  4.如果这些例程均选择不处理异常,如果程序处于被调试状态,操作系统仍会再次挂起程序通知debugger.

  5.如果程序未处于被调试状态或者debugger没有能够处理,并且你调用SetUnhandledExceptionFilter安装了最后异常处理例程的话,系统转向对它的调用.

  6.如果你没有安装最后异常处理例程或者他没有处理这个异常,系统会调用默认的系统处理程序,通常显示一个对话框,你可以选择关闭或者最后将其附加到调试器上的调试按钮.如果没有调试器能被附加于其上或者调试器也处理不了,系统就调用ExitProcess终结程序.

  7.不过在终结之前,系统仍然对发生异常的线程异常处理句柄来一次展开,这是线程异常处理例程最后清理的机会. 如果你看了上面的步骤一头雾水的话,别着急,化点时间慢慢理解或者进入下一部分实例操作.

在实际程序设计过程中,不可能只有一个异常处理例程,这就产生了异常处理程序嵌套的问题,可能很多处理例程分别监视若干子程序并处理其中某种异常,另外一个监视所有子程序可能产生的共性异常,这作起来实际很容易,也方便调试.你只要依次建立异常处理框架就可以了.

  关于VC++异常处理可以嵌套很多人可能比较熟悉,用起来更容易不过实现比这里也就复杂得多,在VC++ 中一个程序所有异常只指向一个相同的处理句例程,然后在这个处理例程里再实现对各个子异常处理例程的调用,他的大致方法是建立一个子异常处理例程入口的数组表,然后根据指针来调用子处理例程,过程比较烦琐,原来打算大致写一点,现在发现自己对C/C++了解实在太少,各位有兴趣还是自己参考MSDN Matt Pietrek 1996年写的一篇文章>,里面有非常详细的说明,对于系统的实现细节也有所讨论,不过相信很多人都没有兴趣.hmmm...:)实际上Kernel的异常处理过程和VC++的很相似.

  有异常嵌套就涉及到异常展开的问题,也许你注意到了如果按照我前面的例子包括final都不处理异常的话, 最后系统在终结程序之前会来一次展开,在试验之后发现,展开不会调用final只是对per_thread例程展开(right?).什么是堆栈展开?为什么要进行堆栈展开?如何进行堆栈展开?

  我曾经为堆栈展开迷惑过,原因是各种资料的描述很不一致,Matt Pietrek说展开后前面的ERR结构被释放, 并且好像链后面如果决定处理必须展开,很多C/C++讲述异常处理的书也如斯说这使人很迷惑,我们再来看看Jeremy Gordon的描述,堆栈展开是处理异常的例程自愿进行的.呵呵,究竟事实如何?

  在迷惑好久之后我终于找到了答案:Matt Pietrek讲的没有错,那是VC++以及系统kernel的处理方法,Jeremy Gordon说的也是正确的,那是我门asm Fans的自由!

  好了现在来说堆栈展开,堆栈展开是异常处理例程在决定处理某个异常的时候给前面不处理这个异常的处理例程的一个清洗的机会,前面拒绝处理这个异常的例程可以释放必要的句柄对象或者释放堆栈或者干点别的工作...

  那完全是你的自由,叫stack unwind似乎有点牵强.堆栈展开有一个重要的标志就是

  EXCEPTION_RECORD.ExceptionFlag为2,表示正在展开,你可以进行相应的处理工作,但实际上经常用的是6这是因为还有一个UNWIND_EXIT什么的,具体含义我也没有搞明白,不过对我们的工作好像没有什么影响.

  注意在自己的异常处理例程中,unwind不是的,必须你自己自觉地引发,如果所有例程都不处理系统最后的展开是注定的,当然如果没有必要你也可以不展开.

 

Structured Exception Handling
责任编辑:ncic   更新日期:2007-4-5

  在所有Win32操作系统提供的机制中,使用最广泛的未公开的机制恐怕就要数结构化异常处理(structuredexceptionhandling,SEH)了。一提到结构化异常处理,可能就会令人想起_try、_finally和_except之类的词儿。在任何一本不错的Win32书中都会有对SEH详细的介绍。甚至连Win32SDK里都对使用_try、_finally和_except进行结构化异常处理作了完整的介绍。既然有这么多地放都提到了SEH,那我为什么还要说它是未公开的呢?本质上讲,Win32结构化异常处理是操作系统提供的一种。编译器的运行时库对这种操作系统实现进行了封装,而所有能找到的介绍SEH的文档讲的都是针对某一特定编译器的运行时库。关键字_try、_finally和_except并没有什么神秘的。微软的OS和编译器定义了这些关键字以及它们的行为。其它的C++编译器厂商也只需要尊从它们定好的语义就行了。在编译器的SEH层减少了直接使用纯操作系统的SEH所带来的危害的同时,也将纯操作系统的SEH从大家的面前隐藏了起来。

  我收到过大量的电子邮件说他们都需要实现编译器级的SEH但却找不到公开的文档。本来,我可以指着VisualC++和BorlangC++的运行时库的源代码说看一下它们就行了。但是,不知道是什么原因,编译器级的SEH仍是个天大的秘密。微软和Borland都没有提供SEH最内层的源代码。

  在本文中,我会从最基本的概念上讲解结构化异常处理。在讲解的时候,我会将操作系统所提供的与编译器代码生成和运行时库支持的分离开来。当深入关键性操作系统程序的代码时,我基于的都是Intel版的WindowsNT4.0。然而。我所讲的大部分内容同样适用于其它的处理器。

  我会避免提及实际的C++的异常处理,C++下用的是catch()而不是_except。其实,真正的C++异常处理的实现方式和我所讲的方式也是极为相似的。但是,真正C++异常处理特有的复杂性会影响到我这里所讲的概念。对于深挖那些晦涩的.H和.INC文件并拼凑出Win32SEH的相关代码,最好的一个信息来源就是IBMOS/2的头文件(特别是BSEXCPT.H)。这对有相关经验的人并没什么可希奇的,这里讲的SEH机制在微软开发OS/2时就定义了。因此,Win32的SEH与OS/2的极为相似。

  SEHintheBuff

  若将SEH的细节都放到一起讨论,任务实在艰巨,因此,我会从简单的开始,一层一层往深里讲。如果之前从未使用过结构化异常处理,则正好心无杂念。若是用过,那就要努力将 _try、GetExceptionCode和

  EXCEPTION_EXECUTE_HANDLER从脑子中扫出,假装这是一个全新的概念。Areyouready?Good。

 

SEH 结构化异常处理(1)
责任编辑:ncic   更新日期:2007-4-5

  [工具]flyod1.10

  [目的]学习SEH的手法,另书中是用SoftICE调试的,看起来不习惯.根据原文内容重新整理一下,便于和我一样的菜鸟们一起学习.

  今天下决心,好好学习,这是就算是个开始吧!感觉学明白的确很不容易!

  [注释]?--为不能理解的地方,请大侠们指点一下.学习过程中,有理解错误的地方,肯请大侠们多多指教.

  [练习对象]加密与加密二版第10章,光盘配套的练习软件:seh.exeseh2.exe

  [writer] ytcswb 2005.2.1 感谢看学及论坛的大侠们为我们提供这么好的学习资料。

  1.例子seh.exe学习:

00401000>$ 8D4424F8  leaeax,dwordptrss:[esp-8]//程序入口!根据下面的代码分析,这里显然可以
                            //理解为开辟8字节的空间,并把栈顶指针保存到eax
                            //相当于subesp,8;leaeax,dwordptrss:[esp]
00401004 . 64:870500000>xchgdwordptrfs:[0],eax  //记住fs[0]永远是指向当前err结构的指针,
                            //执行完成后,fs[0]指向栈顶,准备在堆栈中构造1个err结构
                            //eax等于原fs[0],即指向原来的err结构的指针,即那个err结构的地址
0040100B . BB2E104000 movebx,Seh.0040102E    //地址40102e-->ebx,建议在此地址上设断点,才能正常跟踪入seh代码中
00401010 . 53      pushebx          //压入堆栈,即当前err结构的handler成员,当前异常处理代码的入口地址
00401011 . 50      pusheax          //压入原fs[0],即当前err结构的prev成员,即下一个err结构的地址
此时堆栈:
0012FFBC 0012FFE0 指针到下一个SEH记录//0012FFE0是个指针,看看就知道指向下一个err结构,数值上等于下一个err结构的地址
0012FFC0 0040102E SE句柄       //建立了1个当前的err结构
0012FFE0 FFFFFFFF SEH链尾部
0012FFE4 77E74809 SE句柄
err结构的定义[在Essup.INC源文件中定义的---VC++CRT(CRT含义:C++RunTimelibrary)]:
_EXCEPTION_REGISTERATIONstru
 prev  dd?//指向下一个err结构的指针,数值上等于下一个err结构的首地址(在堆栈中)
 handlerdd?//指向异常处理代码的指针,数值上等于异常处理代码的入口地址即首地址
_EXCEPTION_REGISTERATIONends
00401012 . BE00000000 movesi,0          //简单的赋值语句
00401017 . 8B06     moveax,dwordptrds:[esi] //读取线性地址0,产生异常
                            //执行后,windows检查到异常,执行线程马上被中段,从用户模式转到内核模式
                            //控制权交到操作系统的异常调试程序(exceptiondispatcher),由它负责找到
                            //处理这个异常的方法,即所有应用程序的异常最终都是由windwos来处理的,
                            //同一个版本的windows有固定的异常处理代码.
//如果你把这句nop掉了,也就等于去除了异常.会接着执行到下面的代码,并显示"SEHFail"!
      
00401019 . 6A00    push0                 ;/Style=MB_OK|MB_APPLMODAL
0040101B . 6800304000 pushSeh.00403000            ;|Title="OK"
00401020 . 6810304000 pushSeh.00403010            ;|Text="SEHFail"
00401025 . 6A00    push0                 ;|hOwner=NULL
00401027 . E81C000000 call     ;MessageBoxA
0040102C . EB13    jmpshortSeh.00401041
0040102E . 6A00    push0                 ;/Style=MB_OK|MB_APPLMODAL
00401030 . 6800304000 pushSeh.00403000            ;|Title="OK"
00401035 . 6803304000 pushSeh.00403003            ;|Text="SEHSucceed"
0040103A . 6A00    push0                 ;|hOwner=NULL
0040103C . E807000000 call     ;MessageBoxA
00401041 > 6A00    push0                 ;/ExitCode=0
00401043 . E806000000 call    ;ExitProcess
00401048 $-FF2508204000jmpdwordptrds:[<&USER32.MessageBoxA>]; USER32.MessageBoxA
0040104E .-FF2500204000jmpdwordptrds:[<&KERNEL32.ExitProcess>; kernel32.ExitProcess
00401054   00      db00
00401055   00      db00
---------------------------------------------------------------------------------------------------
00401017 . 8B06     moveax,dwordptrds:[esi] 
//读取线性地址0,产生异常
//执行完这1条指令,od的状态行可以看到,产生了什么异常.状态行的内容如下:
//访问违反:读取[00000000],使用shift+F7/F8/F9键跳过异常以继续执行程序.
//windows检测到了这个异常,就会向堆栈压入3个结构.压入顺序为EXCEPTION_RECORD,EXCEPTION_CONTEXT,EXCEPTION_POINTERS
//EXCEPTION_POINTERS结构就在栈顶,其定义如下:
typedefstrut_EXCEPTION_POINTERS{
+0pEXCEPTION_RECORDExceptionRecordDWORD?//指针,指向EXCEPTION_RECORD结构,即EXCEPTION_RECORD的首地址
+4pCONTEXTContextRecord      DWORD?//指针,指向EXCEPTION_CONTEXT结构,即EXCEPTION_CONTEXT的首地址
}_EXCEPTION_POINTERSends

  在看看EXCEPTION_RECORD结构:

 

SEH 结构化异常处理(2)
责任编辑:ncic   更新日期:2007-4-5

  看堆栈:

0012FCCC 0012FCD4 //指针,指向EXCEPTION_RECORD结构,即EXCEPTION_RECORD的首地址-----这就是EXCEPTION_POINTERS
0012FCD0 0012FCF0 //指针,指向EXCEPTION_CONTEXT结构,即EXCEPTION_CONTEXT的首地址---
0012FCD4 C0000005 ---------------1--异常代码.这里开始就是EXCEPTION_RECORD结构
0012FCD8 00000000
0012FCDC 00000000
0012FCE0 00401018 seh2.00401018 4--异常发生的地址,这就是发生异常的那条指令的地址.
0012FCE4 00000002
0012FCE8 00000000
0012FCEC 00000000
0012FCF0 0001003F ---------------这里开始就是EXCEPTION_CONTEXT结构,ContextFlags
0012FCF4 00000000//dr0
0012FCF8 00000000//dr1
0012FCFC 00000000//dr2
0012FD00 00000000//dr3
0012FD04 0000A000//dr6
0012FD08 00000000//dr7
0012FD0C FFFF027F
77FB4DB3  8B1C24     movebx,dwordptrss:[esp]
77FB4DB6  51       pushecx
77FB4DB7  53       pushebx
77FB4DB8  E8ACBDFAFF  callntdll.77F60B69//f7
77FB4DBD  0AC0      oral,al
77FB4DBF  740C     jeshortntdll.77FB4DCD
77FB4DC1  5B       popebx
77FB4DC2  59       popecx
77FB4DC3  6A00     push0
77FB4DC5  51       pushecx
77FB4DC6  E8480BFCFF  callntdll.ZwContinue
77FB4DCB  EB0B     jmpshortntdll.77FB4DD8
77FB4DCD  5B       popebx
77FB4DCE  59       popecx
77FB4DCF  6A00     push0
77FB4DD1  51       pushecx
77FB4DD2  53       pushebx
77FB4DD3  E8F213FCFF  callntdll.ZwRaiseException
77FB4DD8  83C4EC    addesp,-14
77FB4DDB  890424     movdwordptrss:[esp],eax
77FB4DDE  C744240401000>movdwordptrss:[esp+4],1
77FB4DE6  895C2408   movdwordptrss:[esp+8],ebx
77FB4DEA  C744241000000>movdwordptrss:[esp+10],0
77FB4DF2  54       pushesp
77FB4DF3  E8AFC2F9FF  callntdll.RtlRaiseException
77FB4DF8  C20800    retn8
77FB4DFB>^E97DBCFAFF  jmpntdll.77F60A7D
77F79B7E  55       pushebp
77F79B7F  8BEC      movebp,esp
77F79B81  FF750C    pushdwordptrss:[ebp+C]
77F79B84  52       pushedx
77F79B85  64:FF350000000>pushdwordptrfs:[0]
77F79B8C  64:89250000000>movdwordptrfs:[0],esp
77F79B93  FF7514    pushdwordptrss:[ebp+14]//参数4 _lpDispatchrContext?
77F79B96  FF7510    pushdwordptrss:[ebp+10]//参数3 _lpDContext,指向Context结构
77F79B99  FF750C    pushdwordptrss:[ebp+C] //参数2 _lpSEH,指向ERR结构
77F79B9C  FF7508    pushdwordptrss:[ebp+8] //参数1 _lpExceptionRecord,指向ExceptionRecord结构
77F79B9F  8B4D18    movecx,dwordptrss:[ebp+18]
77F79BA2  FFD1      callecx    ;seh2.00401051转到这里了f7
                      //这就是异常处理回调函数,执行当前异常处理代码即401051处
                      //注:回调函数都是由windows调用的!
                      //看学强调:在此回调函数上设断点,可以轻易地对付一些加壳的反跟踪代码!!!!!
77F79BA4  64:8B250000000>movesp,dwordptrfs:[0]//恢复原来的SEH链表
77F79BAB  64:8F050000000>popdwordptrfs:[0]
77F79BB2  8BE5      movesp,ebp
77F79BB4  5D       popebp
77F79BB5  C21400    retn14
00401051 /$ 55      pushebp                ; Structuredexceptionhandler
00401052 |. 8BEC     movebp,esp
00401054 |. 53      pushebx
00401055 |. 8B4510   moveax,dwordptrss:[ebp+10]//eax是CONTEXT结构的指针
00401058 |. 8D1D2D104000leaebx,dwordptrds:[40102D]//通过修改CONTEXT.EIP,希望到这里执行!
0040105E |. 8998B8000000movdwordptrds:[eax+B8],ebx//修改CONTEXT.EIP,改变程序执行线路,这大概就是利用seh的常用手法!
                             //没改时,是401018即发生异常的指令地址,经过1轮处理又会到这里执行
                             //又产生异常
00401064 |. 33DB     xorebx,ebx
00401066 |. 895804   movdwordptrds:[eax+4],ebx//DR0清零,使断点失效,这大概也是利用seh的常用手法,实现反跟踪!
00401069 |. 895808   movdwordptrds:[eax+8],ebx//DR1
0040106C |. 89580C   movdwordptrds:[eax+C],ebx//DR2
0040106F |. 895810   movdwordptrds:[eax+10],ebx//DR3
00401072 |. C7401855010>movdwordptrds:[eax+18],155//DR7
00401079 |. B800000000 moveax,0//回调处理函数的返回值ExceptionContinueExcetion-->eax
//ExceptionContinueExcetion=0 回调函数返回后,系统将线程环境恢复到_lpContext参数指定的CONTEXT结构并继续执行.
               即,表示已经修复,从异常处继续执行,如果前面没有修改CONTEXT.EIP的值,就会到401018即异常发生处
               继续执行,由于前面修改了CONTEXT.EIP=40102D,所以就转到40102D处继续执行了.
//ExceptionContinueExcetion=1 回调函数拒绝处理这个异常,系统将通过err结构的prev指针得到前一个回掉函数的地址并继续执行它
               也就是转到前一个err结构的异常处理代码处继续执行.
//ExceptionContinueExcetion=2 回调函数在执行中又发生了异常,即嵌套异常
//ExceptionContinueExcetion=3 发生嵌套的展开操作?
0040107E |. 5B      popebx
0040107F |. C9      leave
00401080 . C21000   retn10
[总结]
//看学强调:在此回调函数上设断点,可以轻易地对付一些加壳的反跟踪代码!!!!!
//看学强调:要提前在err结构的handler地址上设断点,否则代码就可能跑飞跟踪seh的关键断点!!!!
//看学提示:可修改CONTEXT结构成员,来实现反跟踪及改变程序流程(设置暗桩吗?)                                 
**************************************************************************

  [附录]跟踪到异常处理回调函数的过程:

阅读(1412) | 评论(0) | 转发(0) |
0

上一篇:IDA反汇编工具初探

下一篇:Windows消息

给主人留下些什么吧!~~