留云山庄庄主 发表于 2018-1-26 12:24:57

调试器学习总结_except_handler4以及栈展开分析

分析这个函数的原因
前段时间任老师讲了安全编程后布置了调试器的项目,随后自己在做项目时候把《软件调试》里面和调试直接相关的几个章节又看了一遍,其它的地方倒还是好说,讲的很详细,而且wrk里面都有源码。但是第24章编译器对try的编译,关于_except_handler4函数就不是很详细了,自己也确实好奇这个函数是如何协调系统和程序员注册的异常处理函数。看了这篇帖子http://bbs.pediy.com/showthread.php?t=140970后,虽然对如何调用自己注册的异常处理函数有了一点了解,但是对栈展开又是一个新问题了。也没看明白,就自己写了个测试程序在ida和od里面一点点读,果然还是以像薛老师说的苦行僧那样去一点点成长,才能对运行原理背后的支撑有清晰的把握。
本文对实际的编程和调试应该没有什么助益,只是自己好奇编译器和系统在背后做了什么而自己做的分析,也算是自己对在15pb学习调试相关知识的一个总结吧。自己思考问题肯定有疏漏,希望大家指正。
本文将按照下面的顺序写。
1.手工注册和用try模型注册的差异。
2.实验程序代码
3._except_handler4函数分析
4.全局展开
5.局部展开
----------------------------------------------------------------------------------------------------------------------------------------
正文

1.两种异常注册方式的差异
对于编译器做了支持的try块,要求我们将异常处理器分成过滤和处理两个部分,由编译器提供的_except_handler4来调用过滤函数,根据过滤函数的返回值(过滤函数的返回值为1、0或者-1)来决定返回EXCEPTION_DISPOSITON类型的值或者调用处理函数。
对于手工注册函数,在SEH链上直接指向了异常处理函数,返回值是直接返回给系统的异常分发函数,所以返回值类型是EXCEPTION_DISPOSITON。
不过和《软件调试》里面关于过滤函数返回值有点不一样,msdn上给出的是:
EXCEPTION_CONTINUE_EXECUTION(-1)    继续执行在发生异常的地方。
EXCEPTION_CONTINUE_SEARCH(0)    异常无法识别。继续搜索堆栈为一个处理程序。EXCEPTION_EXECUTE_HANDLER(1)    异常被识别。通过执行控制转移到异常处理程序__except复合语句,然后继续执行后__except块。
注意:在《软件调试》319页,关于-1和1的解释却是正好相反的,在读_except_handler4代码的时候要注意一点。应该是微软对函数的流程做了改变;或者编译器不一样吧。

2.实验用的程序
(1)程序c代码
#include "stdafx.h"
#include <excpt.h>
void a()
{
int *m = 0;
__try{
    *m = 0;
}
__except (EXCEPTION_CONTINUE_SEARCH){
}
}
void b()
{
__try{
    a();
}
__except (EXCEPTION_CONTINUE_SEARCH){
}
}
void c()
{
__try{
    b();
}
__except (EXCEPTION_EXECUTE_HANDLER){
}
}
int _tmain(int argc, _TCHAR* argv[])
{
c();
return 0;
}


(2)编译器对try块的编译(截取了函数c在OD里的片段)
003C10C0 > $55            PUSH EBP                                 ;c
003C10C1   .8BEC          MOV EBP,ESP
003C10C3   .6A FE         PUSH -2
003C10C5   .68 C0933E00   PUSH ConsoleA.003E93C0//这个就是编译器存放scopeTable的地方
003C10CA   .68 D05B3C00   PUSH ConsoleA._except_handler4//用try模型注册异常,编译器都是用_except_handler4作为直接的异常处理函数。
003C10CF   .64:A1 0000000>MOV EAX,DWORD PTR FS:
003C10D5   .50            PUSH EAX
003C10D6   .83EC 08       SUB ESP,8
003C10D9   .53            PUSH EBX
003C10DA   .56            PUSH ESI
003C10DB   .57            PUSH EDI
003C10DC   .A1 00B03E00   MOV EAX,DWORD PTR DS:
003C10E1   .3145 F8       XOR DWORD PTR SS:,EAX
003C10E4   .33C5          XOR EAX,EBP
003C10E6   .50            PUSH EAX
003C10E7   .8D45 F0       LEA EAX,DWORD PTR SS:
003C10EA   .64:A3 0000000>MOV DWORD PTR FS:,EAX
003C10F0   .8965 E8       MOV DWORD PTR SS:,ESP
003C10F3   .C745 FC 00000>MOV DWORD PTR SS:,0//ebp-4是trylevel,进入第一个try块,所以赋值为0,离开一个try块的时候,编译器也会插入语句改变这个值
003C10FA   .E8 61FFFFFF   CALL ConsoleA.b
003C10FF   .EB 09         JMP SHORT ConsoleA.003C110A
003C1101   .B8 01000000   MOV EAX,1
003C1106   .C3            RETN
003C1107   .8B65 E8       MOV ESP,DWORD PTR SS:
003C110A   >C745 FC FEFFF>MOV DWORD PTR SS:,-2//这个时候就是离开一个try块,赋值为上一层try块的编号。
003C1111   .8B4D F0       MOV ECX,DWORD PTR SS:
003C1114   .64:890D 00000>MOV DWORD PTR FS:,ECX
003C111B   .59            POP ECX
003C111C   .5F            POP EDI
003C111D   .5E            POP ESI
003C111E   .5B            POP EBX
003C111F   .8BE5          MOV ESP,EBP
003C1121   .5D            POP EBP
003C1122   .C3            RETN
3._except_handler4函数分析:
(1)程序的控制从中断如何流动到__except_handler4。
异常分发的流程书上有些介绍,我就不抄过来了。
异常发生时候,KiDispatchException会先将异常发给调试器,调试器不处理的话,会将保存在栈上的PKTRAP_FRAME结构体里面的eip指向KiUserExceptionDispatcher,然后返回。
KiUserExceptionDispatcher会调用RtlDispatchException来寻找SEH处理器。
RtlDispatchException就是循环遍历SEH链,对于栈上的每一个SEH处理器,都调用一次
RtlpExecuteHandlerForException,直到失败或者找到处理块。
RtlpExecuteHandlerForException会将控制一直传递到_except_handler4。
RtlpExecuteHandlerForException->ExecuteHandler->ExecuteHandler2->_except_handler4
当然如果是手工注册的话,ExecuteHandler2就是直接调用我们注册的函数了,而不是编译器在中间插一脚的_except_handler4。
在我们的实验程序中,函数链c->b->a,异常在a中发生,但是b、a不处理,分发到c处理。

(2)对编译器插入的__except_handler4函数本身的分析
本来想直接贴代码,但是好像很长,有五页word的版面吧,想想还是算了。找其中重要的说就是了。
那编译器为什么要插入这个函数呢,看起来手工注册也是可行的。《软件调试》里面也讲过了,重要就是两点:
<1>每次要保护代码都要插入两段汇编代码,来注册和撤销异常处理模块,非常不简洁。
<2>还要求程序要必须要知道context结构体,知道发生异常后能从哪个成员得到可用信息。
为了解决这些问题,编译器就提出try{}except(filter){}模型。对于同一个函数的try块都只构造一次异常栈帧,这些try块中过滤函数和异常处理函数都被存放在一个表格里面,所以__except_handler4这个函数主要功能就是要配合编译器构造的EncodedScopeTable表格,来调用try块的过滤或者处理函数。
函数的流程(函数源码的分析可以参考上面的哪个帖子,我也把自己的分析用word上传了,不过比较乱,我也不知道怎么办才好,好像ida的分析文件单独存在也没法用)


从图中可以看到,_except_handler4函数只返回两个值,0和1,分别表示继续搜索和返回到发生异常的那条指令继续执行。
其中异常标志是用判断本次进入该函数是为了做异常处理还是为了栈展开的处理。
可以看前面编译器对try块的编译,最开始是有一条PUSH -2的操作,这个值所在位置就是下文说的trylevel,如果等于-2表示没有进入该函数的任何try块中,并且在下面的汇编代码中可以看到MOV DWORD PTR SS:,0,说明如果流程执行到这里表示进入第一个try块中,编号是0,这个编号的作用下面会讲。
然后根据顾虑函数是否存在和返回值的不同走向不同的流程。

(3)函数需要注意的几点:
<1>至于为什么判断是否是展开标志用0x66参与运算?我目前只知道#define EXCEPTION_UNWINDING 0x2,只要有这个展开标志,该判断那就成立,说明本次要进入局部展开部分了。其他的标志就没仔细看了。
<2>那在什么情况下进入全局展开呢?就是过滤函数返回1,表示要进入该try块对应的处理部分,并且不会返回了,所以要调用全局展开做一些资源的回收。
<3>那全局展开和局部展开有什么关系呢?全局展开是从SEH链表头开始遍历,每个部分都再一次进入_except_handler4进行局部展开,直到遍历到能够处理该异常的SEH结构为止。
<4>如果一个过滤函数返回1,表示接受该异常,调用全局展开后,栈空间到底是怎么回收到接受该异常的try块呢?这就是全局展开的作用了。

4.全局展开:
先来看看即将调用RtlUnwind时候的堆栈情况。


此时的栈是在RtlExceptionDispatch将异常分发到函数C注册的异常处理器后,_except_handler4做了局部变量初始化之后的栈空间,右边的栈位于左边之上。我们可以看到RtlExceptionDispatch传递过来的参数都指向哪里。
其中EstablishFrame就是标准的SEH链表节点的指针。
RegistrationNode指向函数C在进入try块之前建立的_EH4__EXCEPTION_REGISTRATION_RECORD结构,正好对应的上C建立的栈

_EH4_EXCEPTION_REGISTRATION_RECORD
   +0x000 SavedESP         : Ptr32 Void
   +0x004 ExceptionPointers : Ptr32 _EXCEPTION_POINTERS
   +0x008 SubRecord      : _EXCEPTION_REGISTRATION_RECORD
   +0x010 EncodedScopeTable : Uint4B
   +0x014 TryLevel         : Uint4B

而关于ScopeTable的类型,具体是不知的,参考网上的图片,并且做实验确实能对的上。
而ScopeTableRecord的类型是知道的
ScopeTableRecord
   +0x000 EnclosingLevel   : Uint4B
   +0x004 FilterFunc       : Uint4B
   +0x008 HandlerAddress   : Uint4B
   +0x008 FinallyFunc      : Uint4B

这是做实验时候,第一次进入_except_handler4时候函数A的scopetable表


上面16个字节应该就是cookie之类的值。
下面就是12个字节一个部分了,因为试验中try块没有嵌套,就是一个,所以地址0x010a9390处的值就是-2,表示再上一层就没有try块了。加入有多个嵌套的try块,那么这个值就是上一层的编号,编号顺序是从0开始的。

(1)全局展开相关的代码:
.text:00405BF1mov   ecx, //全局栈展开
.text:00405BF4add   ecx, 8//TargetFrame,指向_EXCEPTION_REGISTRTION_RECORD,也就是标准的SEH链表节点的结构
.text:00405BF7mov   edx, //ExceptionRecord
.text:00405BFAcall    @_EH4_GlobalUnwind2@8
......
.text:00408DE2 @_EH4_GlobalUnwind2@8 proc near
.......(函数@_EH4_GlobalUnwind2@8调用方式是fastcall,所以前两个参数是edx,ecx)
.text:00408DE8push    0               //ReturnValue
.text:00408DEApush    edx             // ExceptionRecord
.text:00408DEBpush    offset ReturnPoint //TargetIp
.text:00408DF0push    ecx             // TargetFrame
.text:00408DF1call    _RtlUnwind@16
.text:00408DF6 ReturnPoint:                           
......
.text:00408DFA @_EH4_GlobalUnwind2@8 endp
......

配合图来看代码就知道,所谓的TargetFrame就是接受该异常的SEH链表节点的指针。然后从fs:开始遍历,直到遍历到TargetFrame所指向的节点。对于每一个节点都要调用
RtlpExecuteHandlerForUnwind,这个函数其实和RtlpExecuteHandlerForException函数一样,最后都会进入到ExecuteHandler2,然后调用_except_handler4。

(2)注意:
<1>虽然a发生的异常自己不处理,但是暂时它所注册的SEH节点并没有被回收。所以fs:此时依然指向a注册的异常处理器。
<2>图中中间的一块显示编译器提供的scopetable表格。
<3>对于RtlUnwind函数的正常返回的方式有点另类。是通过ZwContinue来返回的。一开始进入RtlUnwind函数之后就调用RtlpCaptureContext,这个函数和一般的获取context的函数不同就是进入这个函数后没有改变ebp的值,所以此时ebp还是上面代码call _RtlUnwind时的地址。
RtlpCaptureContext函数对于本次来说重要的代码:
......
76f5b6b0 8b5c2408      mov   ebx,dword ptr //esp+8是局部context变量
......
76f5b71b 8b4504          mov   eax,dword ptr //ebp+4是返回地址,也就是call _RtlUnwind的下一条指令地址,所以通过ZwContinue返回的话,就是这个eip
76f5b71e 8983b8000000    mov   dword ptr ,eax//context+B8是eip,将返回地址赋值给context.eip


<4>在RtlUnwind函数里面还改变了返回后esp的值,挺简单的,wrk源码比较清楚。

(3)假设现在完成了全局展开,再来看看如何让ebp,esp回到接受该异常的try块所在栈(函数C的栈),也就是把以前的栈都回收。
(_except_handler4函数,在调用全局展开函数后执行)
       
......
.text:00405C3Amov   edx, //这里让edx指向了接受该异常的try块所在栈的ebp位置(可以结合图来看,很清楚)
.text:00405C3Dmov   ecx,
.text:00405C40mov   ecx, //ecx等于异常处理函数handler的地址
.text:00405C43call    @_EH4_TransferToHandler@8
......
.text:00408DC9 @_EH4_TransferToHandler@8 proc near
.text:00408DC9mov   ebp, edx      ////此时ebp就指向了接受该异常try块(对于本次实验就是C函数)所在
.text:00408DCBmov   esi, ecx    //esi就是handler地址
......
.text:00408DE0jmp   esi      //跳到handler函数开始执行。
.text:00408DE0 @_EH4_TransferToHandler@8 endp


对于本次实验来讲,jmp esi就是跳到函数C的异常处理块
003C1107   .8B65 E8       MOV ESP,DWORD PTR SS://此时让esp指向当初保存的值
003C110A   >C745 FC FEFFF>MOV DWORD PTR SS:,-2//这个时候就是离开一个try块,赋值为上一层try块的编号。
003C1111   .8B4D F0       MOV ECX,DWORD PTR SS:
003C1114   .64:890D 00000>MOV DWORD PTR FS:,ECX
003C111B   .59            POP ECX
003C111C   .5F            POP EDI
003C111D   .5E            POP ESI
003C111E   .5B            POP EBX
003C111F   .8BE5          MOV ESP,EBP
003C1121   .5D            POP EBP//让ebp指向上一次压栈的值,至此,栈全部回收
003C1122   .C3            RETN

如果大家细心看了编译器对try块的编译,就会看到上述代码的作用,也会看到当初为什么要把esp的值保存下来;而且真正的回收栈的操作也就是pop ebp这一句话而已。

5.局部展开:
先来看看调用__local_unwind4之前的堆栈情况



可以看到RtlUnwind函数第一次调用RtlpExecuteHandlerForException函数之后,再次进入到_except_handler4后,EstablisherFrame指向的是函数A的SEH链表上节点的地址,这也意味着RtlUnwind函数会依次遍历余下的节点,直到和TargetFrame一样。
调用__local_unwind4之前的代码

.text:00405C5Dmov   eax,
.text:00405C60push    eax
.text:00405C61mov   ecx,
.text:00405C64add   ecx, 8
.text:00405C67mov   edx, 0FFFFFFFEh
.text:00405C6Ccall    @_EH4_LocalUnwind@16
......
.text:00408DFB @_EH4_LocalUnwind@16 proc near
......
.text:00408DFBpush    ebp
.text:00408DFCmov   ebp, //这里将ebp的值赋值为a(当然第二次就是b了,下同)的栈基地址,所以下面进入__local_unwind4函数后就是对a函数的栈空间进行操作了。
......
.text:00408E06               call    __local_unwind4
......
.text:00408E0F @_EH4_LocalUnwind@16 endp

这个局部展开的内部代码大体工作是判断是否有finally函数,有的话就调用。

到此算是小规模分析了一番编译器和系统在异常处理这个框架下做了什么工作,我自己感觉是环环相扣。也深深体会到薛老师说的逆向分析很重要的一点就是分析数据结构。其实我自己做的工作微乎其微,假如没有wrk给出的函数原型,我是不可能知道参数的结构体,自然不知道栈中的情况的。
上传一下_except_handler4函数的分析,比较乱,建议还是配合着画的图看上面提到的帖子里的代码,毕竟原作者做了处理。
这是对_except_handler4函数和栈展开的具体分析

GodTsid 发表于 2018-9-30 23:40:32

666666
页: [1]
查看完整版本: 调试器学习总结_except_handler4以及栈展开分析