《Visual C++异常处理机制原理与应用(二)—— C/C++结构化异常处理之try-finally终止处理的使用与原理(下)》

在上一篇文章中,我们其实只分析了终止型异常处理程序中正常的执行流程,这种情况的出现其实需要作如下假设:

  • __try块中的代码执行过程中不会引发异常
  • 这部分代码不会试图提前离开__try块的作用范围(如包含goto、break、continue、return等会导致执行流越出__try块部分的指令)

然而俗话说,“理想很丰满,现实太骨感”,在编程中,不得不考虑到各种各样的情况。当被保护的代码块中出现异常或尝试提前跳出时,按照终止型异常处理程序设立的初衷,都应该让__finally块中的代码得以执行。仔细分析,二者虽然都需要__finally块中的代码有机会执行,但实现原理却不尽相同:

  • 当被保护代码块中引发异常时,OS捕获到该异常并传递到用户层,系统的SEH机制发挥作用,开始遍历FS:[0]指向的EXCEPTION_REGISTRATION结构,找到其中的异常处理函数入口并逐一执行,直到某个异常处理函数报告已处理该异常。

    • 在此情况下,VC++的终止型异常处理程序为了保证即便在__try保护的代码块中出现异常也依然能执行__finally块中的代码,就必须利用系统的SEH机制,在进入__try块前在SEH上注册一个新节点。(这个步骤在上一篇中已经分析过了)

    • 既然在进入__try块时向SEH链上加入了新的节点,那么在终止型异常处理程序结束后,必然也需要将之前向SEH链加入的节点摘掉。否则如果后续代码执行出现异常,系统SEH机制调用到的仍然是之前的处理函数。

    在上一篇文章中,我们其实故意忽略掉了该异常处理段结束后,对SEH链相应节点的摘链操作。这部分对应的反汇编代码如下:

       26:  return 0;
    00E52527 33 C0                xor         eax,eax  
        27: }
    00E52529 52                   push        edx  
    00E5252A 8B CD                mov         ecx,ebp  
    00E5252C 50                   push        eax  
    00E5252D 8D 15 5C 25 E5 00    lea         edx,ds:[0E5255Ch]  
    00E52533 E8 A2 ED FF FF       call        @_RTC_CheckStackVars@8 (0E512DAh)  
    00E52538 58                   pop         eax  
    00E52539 5A                   pop         edx  
    00E5253A 8B 4D F0             mov         ecx,dword ptr [ebp-10h]  
    00E5253D 64 89 0D 00 00 00 00 mov         dword ptr fs:[0],ecx  
    00E52544 59                   pop         ecx  
    00E52545 5F                   pop         edi  
    00E52546 5E                   pop         esi  
    00E52547 5B                   pop         ebx  
    00E52548 81 C4 F0 00 00 00    add         esp,0F0h  
    00E5254E 3B EC                cmp         ebp,esp  
    00E52550 E8 18 EC FF FF       call        __RTC_CheckEsp (0E5116Dh)  
    00E52555 8B E5                mov         esp,ebp  
    00E52557 5D                   pop         ebp  
    00E52558 C3                   ret  

    这段代码主要进行了以下4项工作:

    1. 清空eax寄存器,返回0
    2. 根据函数入口处在栈上设置的Cookie探针值,检查该值是否被修改(防止栈溢出攻击覆盖掉其下方的返回地址)
    3. 对之前加入SEH链的新节点执行摘链操作:从[ebp-0x10]处取得后继SEH节点的地址并赋给FS:[0]
    4. 校验堆栈平衡,确保esp的值在执行函数执行过程中保持了平衡(利用ebp作为参照)
  • 当被保护代码块包含有试图提前跳出__try块的指令时,由于并没有异常发生,此时只能依靠编译器自己检测到这些指令,并在执行它们前让__finally块中的代码有机会被执行。

下面就来分析一下__try块中含有试图提早退出的代码。

DWORD funcTest02()
{
    DWORD dwTemp = 3;
    __try 
    {
        dwTemp = 5;
        cout << "before return:" << dwTemp << endl;
        return dwTemp;
        cout << "after return:" << dwTemp << endl;
    }
    __finally
    {
        if (AbnormalTermination())
        {
            cout << "__try块中执行时提前退出了" << endl;
        }
        else
        {
            cout << "执行流程自然转到了__finally块中" << endl;
        }
        cout << "enter finally:" << dwTemp << endl;
        dwTemp = 10;
        cout << "before exit finally:" << dwTemp << endl;
    }
    cout << "after finally:" << dwTemp << endl;
}

该函数的执行结果如下:

这里写图片描述

其反汇编代码如下:

   28: 
    29: DWORD funcTest02()
    30: {
00E525D0 55                   push        ebp  
00E525D1 8B EC                mov         ebp,esp  
00E525D3 6A FE                push        0FFFFFFFEh  
00E525D5 68 E8 AF E5 00       push        0E5AFE8h  
00E525DA 68 D0 2C E5 00       push        offset _except_handler4 (0E52CD0h)  
00E525DF 64 A1 00 00 00 00    mov         eax,dword ptr fs:[00000000h]  
00E525E5 50                   push        eax  
00E525E6 81 C4 14 FF FF FF    add         esp,0FFFFFF14h  
00E525EC 53                   push        ebx  
00E525ED 56                   push        esi  
00E525EE 57                   push        edi  
00E525EF 8D BD 04 FF FF FF    lea         edi,[ebp-0FCh]  
00E525F5 B9 39 00 00 00       mov         ecx,39h  
00E525FA B8 CC CC CC CC       mov         eax,0CCCCCCCCh  
00E525FF F3 AB                rep stos    dword ptr es:[edi]  
00E52601 A1 00 C0 E5 00       mov         eax,dword ptr [__security_cookie (0E5C000h)]  
00E52606 31 45 F8             xor         dword ptr [ebp-8],eax  
00E52609 33 C5                xor         eax,ebp  
00E5260B 50                   push        eax  
00E5260C 8D 45 F0             lea         eax,[ebp-10h]  
00E5260F 64 A3 00 00 00 00    mov         dword ptr fs:[00000000h],eax  //在SEH链头注册新节点
    31:     DWORD dwTemp = 3;
00E52615 C7 45 E0 03 00 00 00 mov         dword ptr [dwTemp],3  
    32:     __try 
; <<<<<<<<<<<<<<<<<<<<<<<<<<<<下面将状态标志置为0了!<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
00E5261C C7 45 FC 00 00 00 00 mov         dword ptr [ebp-4],0
; <<<<<<<<<<<<<<<<<<<<<<<<<<<<下面将提前退出标记置为true!<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
00E52623 C7 85 08 FF FF FF 01 00 00 00 mov         dword ptr [ebp-0F8h],1
    33:     {
    34:         dwTemp = 5;
00E5262D C7 45 E0 05 00 00 00 mov         dword ptr [dwTemp],5  
    35:         cout << "before return:" << dwTemp << endl;
; <<<<<<<<<<<<<<<<<这里省略一大堆输出语句对应的汇编代码<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< 
    36:         return dwTemp;
00E52673 8B 45 E0             mov         eax,dword ptr [dwTemp]  
00E52676 89 85 14 FF FF FF    mov         dword ptr [ebp-0ECh],eax 
; <<<<<<<<<<<<<<<<<上面两句将返回值放入临时变量保存,然后准备执行局部展开<<<<<<<<<<<<<<<<<<<<<<<<<<<< 
00E5267C 6A FE                push        0FFFFFFFEh        ;
00E5267E 8D 4D F0             lea         ecx,[ebp-10h]  
00E52681 51                   push        ecx  
00E52682 68 00 C0 E5 00       push        offset __security_cookie (0E5C000h)  
00E52687 E8 B0 E9 FF FF       call        __local_unwind4 (0E5103Ch)  
; <<<<<<<<<<<<<<<<<void __cdecl __local_unwind4(安全Cookie地址,本SEH节点地址,-2)<<<<<<<<<<<<<<<<<
    36:         return dwTemp;
00E5268C 83 C4 0C             add         esp,0Ch  
; <<<<<<<<<<<<<<<<<完成局部展开后,从局部变量中恢复保存的返回值<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
00E5268F 8B 85 14 FF FF FF    mov         eax,dword ptr [ebp-0ECh] 
; <<<<<<<<<<<<<<<<<至此__try块执行结束,准备跳过__finally块执行卸载本SEH节点的代码<<<<<<<<<<<<<<<<<
00E52695 E9 7C 01 00 00       jmp         $LN10+3Fh (0E52816h)  
    37:         cout << "after return:" << dwTemp << endl;
; <<<<<<<<<<<<<<<<<这里省略一大堆输出语句对应的汇编代码<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
    38:     }
; <<<<<<<<<<<<<<<<<如果能执行到这里,证明__try中代码没发生异常,标志改回-2<<<<<<<<<<<<<<<<<<<<<<<<<<
00E526D9 C7 45 FC FE FF FF FF mov         dword ptr [ebp-4],0FFFFFFFEh  
; <<<<<<<<<<<<<<<<<如果能执行到这里,证明__try中代码没发生异常,提前退出标志置为false<<<<<<<<<<<<<<<<
00E526E0 C7 85 08 FF FF FF 00 00 00 00 mov         dword ptr [ebp-0F8h],0  
; <<<<<<<<<<<<<<<<<正常过渡到__finally代码块中,一条call指令就到了__finally块包装的函数中<<<<<<<<<<<
00E526EA E8 05 00 00 00       call        funcTest02+124h (0E526F4h)  
; <<<<<<<<<<<<<<<<<执行完__finally中包装的函数返回后,jmp到__finally块之后继续执行(主要是卸载本SEH节点)<<<<<<<<<<<
00E526EF E9 E3 00 00 00       jmp         $LN10 (0E527D7h)  
    39:     __finally
    40:     {
    41:         if (AbnormalTermination())
00E526F4 83 BD 08 FF FF FF 00 cmp         dword ptr [ebp-0F8h],0  
00E526FB 74 2B                je          funcTest02+158h (0E52728h)  
    42:         {
    43:             cout << "__try块中执行时提前退出了" << endl;
; <<<<<<<<<<<<<<<<<这里省略一大堆输出语句对应的汇编代码<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< 
    44:         }
    45:         else
00E52726 EB 29                jmp         funcTest02+181h (0E52751h)  
    46:         {
    47:             cout << "执行流程自然转到了__finally块中" << endl;
; <<<<<<<<<<<<<<<<<这里省略一大堆输出语句对应的汇编代码<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
    48:         }
    49:         cout << "enter finally:" << dwTemp << endl;
; <<<<<<<<<<<<<<<<<这里省略一大堆输出语句对应的汇编代码<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<  
    50:         dwTemp = 10;
; <<<<<<<<<<<<<<<<<这里虽然修改了dwTemp的值,但由于在__try中已将返回值写到了局部变量中,所以无济于事<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< 
00E52790 C7 45 E0 0A 00 00 00 mov         dword ptr [dwTemp],0Ah  
    51:         cout << "before exit finally:" << dwTemp << endl;
; <<<<<<<<<<<<<<<<<这里省略一大堆输出语句对应的汇编代码<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
$LN14:
00E527D6 C3                   ret  
    52:     }
    53:     cout << "after finally:" << dwTemp << endl;
; <<<<<<<<<<<<<<<<<这里省略一大堆输出语句对应的汇编代码<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
    54: }
00E52816 52                   push        edx  
00E52817 8B CD                mov         ecx,ebp  
00E52819 50                   push        eax  
00E5281A 8D 15 48 28 E5 00    lea         edx,ds:[0E52848h]  
00E52820 E8 B5 EA FF FF       call        @_RTC_CheckStackVars@8 (0E512DAh)  
00E52825 58                   pop         eax  
00E52826 5A                   pop         edx  
00E52827 8B 4D F0             mov         ecx,dword ptr [ebp-10h]  
00E5282A 64 89 0D 00 00 00 00 mov         dword ptr fs:[0],ecx  
00E52831 59                   pop         ecx  
00E52832 5F                   pop         edi  
00E52833 5E                   pop         esi  
00E52834 5B                   pop         ebx  
00E52835 81 C4 FC 00 00 00    add         esp,0FCh  
00E5283B 3B EC                cmp         ebp,esp  
00E5283D E8 2B E9 FF FF       call        __RTC_CheckEsp (0E5116Dh)  
00E52842 8B E5                mov         esp,ebp  
00E52844 5D                   pop         ebp  
00E52845 C3                   ret  

关键地方都用注释的方式给出了分析。从中可以发现,编译器在保护代码块中发现试图提前跳出__try块的return语句后,先是用局部变量将返回值保存起来,然后采用局部展开的方法,调用了__cdecl约定的__local_unwind4函数,并依次传入了“安全Cookie地址”,“本SEH节点地址”,“-2”三个参数。而在该函数内,又调用了__finally块包装的函数,当其执行时,调用栈如下:

这里写图片描述

需要注意的是,调用__local_unwind4时并没有传入__finally块下函数的入口地址。那下面就来看看这个__local_unwind4是怎么找到并调用该函数的:

.text:10002C80 ; Exported entry  60. _local_unwind4
.text:10002C80
.text:10002C80 ; =============== S U B R O U T I N E =======================================
.text:10002C80
.text:10002C80
.text:10002C80 ; int __cdecl _local_unwind4(int pCookie, int pExceptionRegistration, int tryLevel)
.text:10002C80                 public __local_unwind4
.text:10002C80 __local_unwind4 proc near               ; CODE XREF: _unwind_handler4+2Dp
.text:10002C80                                         ; _seh_longjmp_unwind4(x)+1Cp ...
.text:10002C80
.text:10002C80 CookiePointer   = dword ptr -20h
.text:10002C80 pCookie         = dword ptr  4
.text:10002C80 pExceptionRegistration= dword ptr  8
.text:10002C80 tryLevel        = dword ptr  0Ch
.text:10002C80
.text:10002C80                 push    ebx
.text:10002C81                 push    esi
.text:10002C82                 push    edi
.text:10002C83                 mov     edx, [esp+0Ch+pCookie]
.text:10002C87                 mov     eax, [esp+0Ch+pExceptionRegistration]
.text:10002C8B                 mov     ecx, [esp+0Ch+tryLevel]
.text:10002C8F                 push    ebp
.text:10002C90                 push    edx
.text:10002C91                 push    eax
.text:10002C92                 push    ecx
.text:10002C93                 push    ecx
.text:10002C94                 push    offset _unwind_handler4
.text:10002C99                 push    large dword ptr fs:0
.text:10002CA0                 mov     eax, ___security_cookie
.text:10002CA5                 xor     eax, esp
.text:10002CA7                 mov     [esp+28h+CookiePointer], eax
.text:10002CAB                 mov     large fs:0, esp
.text:10002CB2
.text:10002CB2 _lu_top:                                ; CODE XREF: __local_unwind4+64j
.text:10002CB2                                         ; __local_unwind4+80j
.text:10002CB2                 mov     eax, [esp+28h+pExceptionRegistration]
.text:10002CB6                 mov     ebx, [eax+_EH3_EXCEPTION_REGISTRATION.ScopeTable]
.text:10002CB9                 mov     ecx, [esp+28h+pCookie]
.text:10002CBD                 xor     ebx, [ecx]      ; 再次与Cookie异或解密出ScopeTable
.text:10002CBF                 mov     esi, [eax+_EH3_EXCEPTION_REGISTRATION.TryLevel]
.text:10002CC2                 cmp     esi, 0FFFFFFFEh ; 这里由于在最外层__try块内,TryLevel值为0
.text:10002CC5                 jz      short _lu_done_local_unwind_done ; 如果在未经保护的代码块中即tryLevel为-2,则直接退出。
.text:10002CC7                 mov     edx, [esp+28h+tryLevel]
.text:10002CCB                 cmp     edx, 0FFFFFFFEh ; 由于调用该函数时传入的TryLevel值为-2
.text:10002CCE                 jz      short loc_10002CD4 ; 这里准备查ScopeTable了
.text:10002CCE                                         ; 注意,这里esi就是ER4结构中夹带的TryLevel值,目前在__try内,为0
.text:10002CD0                 cmp     esi, edx
.text:10002CD2                 jbe     short _lu_done_local_unwind_done
.text:10002CD4
.text:10002CD4 loc_10002CD4:                           ; CODE XREF: __local_unwind4+4Ej
.text:10002CD4                 lea     esi, [esi+esi*2] ; 这里准备查ScopeTable了
.text:10002CD4                                         ; 注意,这里esi就是ER4结构中夹带的TryLevel值,目前在__try内,为0
.text:10002CD7                 lea     ebx, [ebx+esi*4+10h] ; ebx指向ScopeTableRecord
.text:10002CD7                                         ; 结合上一句esi *= 3可知,ScopeTableRec中一个表项包含3个DWORD。
.text:10002CDB                 mov     ecx, [ebx]      ; 这里取到了EnclosingLevel
.text:10002CDD                 mov     [eax+_EH3_EXCEPTION_REGISTRATION.TryLevel], ecx ; 将指针指向的值保存到夹带ER4中的TryLevel字段
.text:10002CDD                                         ; 这里ecx(EnclosingLevel)值为-2,其实就是设置了退出条件,在下一次循环后就退出了
.text:10002CE0                 cmp     dword ptr [ebx+4], 0 ; 该指针指向的是一个结构,第二个字段需要和0比较(FilterFunc)
.text:10002CE0                                         ; 这里确实为0
.text:10002CE4                 jnz     short _lu_top
.text:10002CE6                 push    101h
.text:10002CEB                 mov     eax, [ebx+8]    ; 这里取到的就是__finally块中的地址
.text:10002CEE                 call    __NLG_Notify    ; 将__finally函数地址、传入的参数、当前ebp保存到vcruntime140d._NLG_Destination结构中
.text:10002CF3                 mov     ecx, 1
.text:10002CF8                 mov     eax, [ebx+8]
.text:10002CFB                 call    __NLG_Call      ; 该函数只有一个功能,就是call eax然后就ret
.text:10002D00                 jmp     short _lu_top
.text:10002D02 ; ---------------------------------------------------------------------------
.text:10002D02
.text:10002D02 _lu_done_local_unwind_done:             ; CODE XREF: __local_unwind4+45j
.text:10002D02                                         ; __local_unwind4+52j
.text:10002D02                 pop     large dword ptr fs:0
.text:10002D09                 add     esp, 18h
.text:10002D0C                 pop     edi
.text:10002D0D                 pop     esi
.text:10002D0E                 pop     ebx
.text:10002D0F                 retn
.text:10002D0F __local_unwind4 endp

对应的流程图如下:

这里写图片描述

通过动态调试与静态分析相结合,在该函数中最终是通过ScopeTable定位到__finally包装的函数的。这张表位于标准EXCEPTION_REGISTERATION结构后一点,属于VC++夹带的内容。将这部分逆向为C语言后大致相当于如下代码:

#include <windows.h>

typedef  struct _EH4_SCOPETABLE_RECORD {
    DWORD EnclosingLevel;
    long(*FilterFunc)();
    union {
        void(*HandlerAddress)();
        void(*FinallyFunc)();
    };
}EH4_SCOPETABLE_RECORD, *PEH4_SCOPETABLE_RECORD;

typedef struct _EH4_SCOPETABLE {
    UINT GSCookieOffset;
    UINT GSCookieXOROffset;
    UINT EHCookieOffset;
    UINT EHCookieXOROffset;
    _EH4_SCOPETABLE_RECORD ScopeRecord[1];
}EH4_SCOPETABLE, *PEH4_SCOPETABLE;

typedef struct _EH4_EXCEPTION_REGISTRATION
{
    UINT prev;
    UINT fnHandler;
    PEH4_SCOPETABLE ScopeTableWithCookieXor;
    DWORD TryLevel;
}EH4_EXCEPTION_REGISTRATION, *PEH4_EXCEPTION_REGISTRATION;

int __cdecl _local_unwind4(PDWORD pdwCookie, PEH4_EXCEPTION_REGISTRATION pExceptionRegistration, DWORD tryLevel)
{
    _asm
    {
        push ebp
        push pdwCookie
        push pExceptionRegistration
        push tryLevel
        push tryLevel
        push offset _unwind_handler4
        push dword ptr fs:[0]
        mov fs:[0], esp
    }

    while (1)
    {
        DWORD dwTryLevelInER = pExceptionRegistration->TryLevel;
        PEH4_SCOPETABLE ScopeTable = pExceptionRegistration->ScopeTableWithCookieXor ^ *(pdwCookie);
        // 如果当前不在任何try块中(tryLevel为-2)或者当前try块在传入的tryLevel的外面(越向外数字越小)
        if (dwTryLevelInER == -2 || (tryLevel != -2 && dwTryLevelInER <= tryLevel))
        {
            _asm
            {
                pop fs:[0]
                add esp,0x18
            }
            return;
        }
        else
        {
            dwTryLevelInER *= 3;
            PEH4_SCOPETABLE_RECORD psr = ScopeTable[dwTryLevelInER].ScopeRecord;
            pExceptionRegistration->TryLevel = psr[0].EnclosingLevel;
            if (psr[0].FilterFunc == NULL)
            {
                //_asm mov eax, psr[0].FinallyFunc
                //__NLG_Notify(0x101h);
                //_asm mov ecx, 1
                //_asm mov eax, psr[0].FinallyFunc
                //__NLG_Call();
                psr[0].FinallyFunc();
            }
        }   
    }
}

2017.12.03日补充(之前忘记对该函数行为进行归纳了)这里可以对_local_unwind4函数的行为做一个小结:从当前try块的等级开始(本例是0),一直向外到传入的dwTryLevel(本例是-2)为止,如果这期间有finally块,执行其中的代码(本例只执行1次)。

用我们自己逆出来的my_local_unwind4函数代替VS2017的_local_unwind4函数,看看能不能正常工作。注意,我们自己的my_local_unwind4函数相比于真正的_local_unwind4函数,有如下差异:

  • 原函数入口处安装了一个以_unwind_handler4为处理函数的SEH节点,模拟函数将该字段置为NULL
  • 原函数调用了__NLG_Notify函数和__NLG_Call函数,由__NLG_Call调用finally函数,模拟函数进行了省略,直接调用了finally函数。
  • 原函数不对ebp做改动,因此使用的是主调函数的栈帧。这个问题后面会讨论。
#include<windows.h>
#include<iostream>
using namespace std;

#include <windows.h>

typedef  struct _EH4_SCOPETABLE_RECORD {
    DWORD EnclosingLevel;
    long(*FilterFunc)();
    union {
        void(*HandlerAddress)();
        void(*FinallyFunc)();
    };
}EH4_SCOPETABLE_RECORD, *PEH4_SCOPETABLE_RECORD;

typedef struct _EH4_SCOPETABLE {
    UINT GSCookieOffset;
    UINT GSCookieXOROffset;
    UINT EHCookieOffset;
    UINT EHCookieXOROffset;
    _EH4_SCOPETABLE_RECORD ScopeRecord[1];
}EH4_SCOPETABLE, *PEH4_SCOPETABLE;

typedef struct _EH4_EXCEPTION_REGISTRATION
{
    UINT prev;
    UINT fnHandler;
    PEH4_SCOPETABLE ScopeTableWithCookieXor;
    DWORD TryLevel;
}EH4_EXCEPTION_REGISTRATION, *PEH4_EXCEPTION_REGISTRATION;

void __cdecl my_local_unwind4(PDWORD pdwCookie, PEH4_EXCEPTION_REGISTRATION pExceptionRegistration, DWORD tryLevel)
{
    _asm
    {
        push ebp
        push pdwCookie
        push pExceptionRegistration
        push tryLevel
        push tryLevel
        //push offset _unwind_handler4
        push 0
        push dword ptr fs : [0]
        mov fs : [0], esp
    }

    while (1)
    {
        UINT dwTryLevelInER = pExceptionRegistration->TryLevel;
        PEH4_SCOPETABLE ScopeTable = (PEH4_SCOPETABLE)((DWORD)(pExceptionRegistration->ScopeTableWithCookieXor) ^ *(pdwCookie));
        if (dwTryLevelInER == -2 || (tryLevel != -2 && dwTryLevelInER <= tryLevel))
        {
            _asm
            {
                pop fs : [0]
                add esp, 0x18
            }
            return;
        }
        else
        {
            dwTryLevelInER *= 3;
            PEH4_SCOPETABLE_RECORD psr = ScopeTable[dwTryLevelInER].ScopeRecord;
            pExceptionRegistration->TryLevel = psr[0].EnclosingLevel;
            if (psr[0].FilterFunc == NULL)
            {
                //_asm mov eax, psr[0].FinallyFunc
                //__NLG_Notify(0x101h);
                //_asm mov ecx, 1
                //_asm mov eax, psr[0].FinallyFunc
                //__NLG_Call();
                // psr[0].FinallyFunc();  //由于调用时ebp的值已经改变,所以在其中访问临时变量出错
                UINT uHandler = (UINT)(psr[0].FinallyFunc);
                _asm
                {
                    push eax
                    mov eax, uHandler
                    push ebp
                    mov ebp, [ebp]
                    call eax
                    pop ebp
                    pop eax
                }
            }
        }
    }
}

DWORD funcTest01()
{
    DWORD dwTemp = 3;
    __try
    {
        dwTemp = 4;
    }
    __finally
    {
        dwTemp = 6;
        _asm mov[ebp - 0xEC], 1
        if (AbnormalTermination())
        {
            cout << "__try块中执行时提前退出了" << endl;
        }
        else
        {
            cout << "执行流程自然转到了__finally块中" << endl;
        }
    }
    cout << dwTemp << endl;
    return 0;
}

DWORD funcTest02()
{
    DWORD dwTemp = 3;
    DWORD dwTempRet = 0;
    __try 
    {
        dwTemp = 5;
        cout << "before return:" << dwTemp << endl;
        //return dwTemp;
        //cout << "after return:" << dwTemp << endl;
        dwTempRet = dwTemp;
        _asm
        {
            push -2
            push fs : [0]
            push offset __security_cookie
            call my_local_unwind4
            add esp, 0xC
        }
        dwTemp = dwTempRet;
        _asm
        {
            jmp afterfinally
        }
    }
    __finally
    {
        if (AbnormalTermination())
        {
            cout << "__try块中执行时提前退出了" << endl;
        }
        else
        {
            cout << "执行流程自然转到了__finally块中" << endl;
        }
        cout << "enter finally:" << dwTemp << endl;
        dwTemp = 10;
        cout << "before exit finally:" << dwTemp << endl;
    }
afterfinally:
    cout << "after finally:" << dwTemp << endl;
}

int main()
{
    funcTest01();
    cout<<funcTest02();
    return 0;
}

然而运行后,程序在执行完finally函数时发生异常,经检查发现问题如下:

在局部展开函数中调用了finally函数后,由编译器插入了一个堆栈平衡校验,其原理是在调用finally函数前将esp的值复制到esi中,在函数调用结束后,再对二者进行比较,如果相等,则函数调用与返回后堆栈是平衡的,如果不相等,则抛异常。

    77:                 psr[0].FinallyFunc();
00DF29ED B8 0C 00 00 00       mov         eax,0Ch  
00DF29F2 6B C8 00             imul        ecx,eax,0  
00DF29F5 8B 55 E0             mov         edx,dword ptr [ebp-20h]  
00DF29F8 8B F4                mov         esi,esp  
00DF29FA 8B 44 0A 08          mov         eax,dword ptr [edx+ecx+8]  
00DF29FE FF D0                call        eax  
00DF2A00 3B F4                cmp         esi,esp  
00DF2A02 E8 61 E7 FF FF       call        __RTC_CheckEsp (0DF1168h) 

然而编译器生成的finally函数中并没有保护esi的值就直接使用了该寄存器,导致函数返回后原先esi的值已经改变,所以当__RTC_CheckEsp执行后检测到esi值与esp值不等,必然抛异常。解决方案就是关闭编译器的运行时检查。

另外,仔细观察就会发现,用我们自己的局部展开函数调用finally函数后,其ebp值发生了变化,导致finally函数中无法访问到使用ebp进行索引的局部变量。这也是在__local_unwind4中没有使用ebp索引,甚至都没动过ebp的原因了。在Visual C++中,编译时有一个/Oy选项用于指定是否使用栈帧指针(即ebp),但这里的问题在于:

  • 如果省略整个工程的栈帧指针,则仅使用esp寻址,一样会出现esp发生变化的问题。
  • 只省略my局部展开函数所在模块的栈帧指针,编译后发现依然在用ebp寻址。

所以只有靠我们自己想办法了,一个自然而然的想法是在进入my局部展开函数时就将ebp的值保存下来,在调用finally函数前对现有ebp的值进行备份,并在调用完后恢复。但实际操作起来比较麻烦:

  • 需要将my局部展开函数定义为裸函数,在入口处完成对ebp的保存。则后续现场保护与恢复都需要我们自己来做。

  • 如果是保存到局部变量中,在恢复前又将当前ebp值存入该变量中,name调用完finally函数后如何从局部变量中恢复ebp的值呢?(局部变量寻址依赖ebp,而ebp已经被改了)

  • 保存到全局变量中为了多线程安全又要加锁,为了避免加锁使用TLS存储又需要额外的代码。

    UINT uHandler = (UINT)(psr[0].FinallyFunc);
    _asm
    {
        push eax
        mov eax, uHandler
        push ebp
        mov ebp, [ebp]  //函数入口是push ebp, mov ebp,esp,因此这里拿到是上一帧的ebp值
        call eax
        pop ebp
        pop eax
    }

    经过上述修改,我们的my__local_unwind4函数终于达到了以假乱真的效果。

这里写图片描述

问题:如果在__finally块中又发生了异常怎么办?

下面分析如下几种情况:

  1. 保护代码块中有return语句,finally函数中无return语句

    这种情况就是我们刚才分析过的情况,保护块中的返回语句不会被翻译为ret指令,而是将返回值暂存在局部变量中,然后通过局部展开函数调用finally函数。局部展开完成后,用jmp指令跳到finally函数后,卸载掉本SEH节点,然后从局部变量中取回保存的返回值并返回。

  2. 保护代码块中有return语句,finally函数中有return语句

    这种情况下,保护块中返回语句依然只是将返回值暂存到局部变量中,然后通过局部展开调用finally函数,而finally函数中的return语句首先会被翻译为对该局部变量的赋值语句,接着JMP到函数体以外,执行一段汇编代码,恢复esp的值(ebp-0x18位置暂存)、卸载本SEH节点、从局部变量中取出返回值,然后返回。即直接在finally函数中返回(会有一段代码在函数体之外)

    对应的代码如下

    DWORD funcTest04()
    {
    __try
    {
        return 3;
    }
    __finally
    {
        if (AbnormalTermination())
        {
            cout << "__try块中执行时提前退出了" << endl;
        }
        else
        {
            cout << "执行流程自然转到了__finally块中" << endl;
        }
        return 4;
    }
    return 5;
    }

    这样,在try之前就需要保存esp的值:

    01072560 55                   push        ebp  
    01072561 8B EC                mov         ebp,esp  
    01072563 6A FE                push        0FFFFFFFEh  
    01072565 68 48 84 07 01       push        1078448h  
    0107256A 68 40 27 07 01       push        offset _except_handler4 (01072740h)  
    0107256F 64 A1 00 00 00 00    mov         eax,dword ptr fs:[00000000h]  
    01072575 50                   push        eax  
    01072576 83 C4 AC             add         esp,0FFFFFFACh  
    01072579 53                   push        ebx  
    0107257A 56                   push        esi  
    0107257B 57                   push        edi  
    0107257C A1 00 90 07 01       mov         eax,dword ptr [__security_cookie (01079000h)]  
    01072581 31 45 F8             xor         dword ptr [ebp-8],eax  
    01072584 33 C5                xor         eax,ebp  
    01072586 50                   push        eax  
    01072587 8D 45 F0             lea         eax,[ebp-10h]  
    0107258A 64 A3 00 00 00 00    mov         dword ptr fs:[00000000h],eax  
    01072590 89 65 E8             mov         dword ptr [ebp-18h],esp  

    而在finally函数中,本来主调者是__local_unwind4函数,经过如下语句后,栈已经切换回进入保护代码块之前的状态了:

      121:      return 4;
    01072631 C7 45 A0 04 00 00 00 mov         dword ptr [ebp-60h],4      ; 临时返回变量值设为4
    01072638 8B 65 E8             mov         esp,dword ptr [ebp-18h]    ; 栈切换进入try之前
    0107263B 8B 45 A0             mov         eax,dword ptr [ebp-60h]     ; 从临时返回变量中取值
    0107263E EB 06                jmp         $LN10+5h (01072646h)       ; 跳出函数体
    $LN11:
    01072640 C3                   ret  
      122:  }
      123:  return 5;
    01072641 B8 05 00 00 00       mov         eax,5  
      124: }
    01072646 8B 4D F0             mov         ecx,dword ptr [ebp-10h] ;卸载SEH节点,恢复现场,返回
    01072649 64 89 0D 00 00 00 00 mov         dword ptr fs:[0],ecx  
    01072650 59                   pop         ecx  
    01072651 5F                   pop         edi  
    01072652 5E                   pop         esi  
    01072653 5B                   pop         ebx  
    01072654 8B E5                mov         esp,ebp  
    01072656 5D                   pop         ebp  
    01072657 C3                   ret  

    注意,中途的ret和mov eax,5都没有执行而是被跳过了。

  3. 保护代码块中无return,finally函数中有return

    这种情况下,在进入保护代码块前会保存esp的值,保护代码块中没有提前退出的代码,因此不触发局部展开,执行流程自然转到finally函数中(通过call指令),而在finally函数中,return语句被翻译为对局部变量的赋值,然后恢复esp到try之前的状态、从局部变量中取到返回值给eax、跳到异常处理块后执行卸载SEH节点与返回操作。

至此,可以小结一下了。

  • 如果在保护代码块中有尝试提早越出的代码,编译器发现后会在执行前加入局部展开函数的代码,由其调用finally函数。展开后再执行这些越出操作。
  • 在保护块中的return语句并不会直接生成ret指令,而是将返回值放入局部变量中,然后执行局部展开操作。执行完局部展开操作后,再jmp到异常处理函数结束处,卸载掉SEH节点,并从局部变量中取出返回值返回。
  • finally块中的return语句会对临时返回变量进行赋值,然后切换esp到进入try之前的状态。然后jmp到异常处理函数结束处,卸载掉SEH节点,并从局部变量中取出返回值返回。
  • 对于终止线程、终止进程的情况,finally函数可能就不会有执行机会了,毕竟执行流已经不在了。

由此可见,相较于自然退出(try中的代码能善始善终地执行完,而不出现异常或者试图提前离开try块的情况),提前离开会带来一定性能和效率上的损失,因为这需要执行退出展开。为此,VC++增加了一个名为__leave的关键字,该关键字被翻译为跳转到try末尾处,然后执行将标志设为-2,异常退出标志设为false,然后调用finally函数:

   132:             __leave;
00FA4609 EB 22                jmp         funcTest04+8Dh (0FA462Dh)  
   133:         }
                    //省略若干代码
   138:     }
00FA462D C7 45 FC FE FF FF FF mov         dword ptr [ebp-4],0FFFFFFFEh  
00FA4634 C7 45 A0 00 00 00 00 mov         dword ptr [ebp-60h],0  
   137:         }
   138:     }
00FA463B E8 02 00 00 00       call        funcTest04+0A2h (0FA4642h)  
00FA4640 EB 58                jmp         $LN12 (0FA469Ah) 
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值