1. 程式人生 > >《Visual C++異常處理機制原理與應用(二)—— C/C++結構化異常處理之try-finally終止處理的使用與原理(下)》

《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  
    
                
               

    相關推薦

    Visual C++異常處理機制原理應用—— C/C++結構異常處理try-finally終止處理的使用原理

    在上一篇文章中,我們其實只分析了終止型異常處理程式中正常的執行流程,這種情況的出現其實需要作如下假設: __try塊中的程式碼執行過程中不會引發異常 這部分程式碼不會試圖提前離開__try塊的作用範圍(如包含goto、break、continue、retur

    COM元件設計應用(十)——錯誤異常處理

    一、前言   程式設計中,錯誤處理必不可少,而且通常要佔用很大的篇幅。本回書著落在 COM 中的錯誤(異常)的處理方法。   在元件程式中,如果遇到錯誤,一般有兩個方式進行處理。   二、簡單返回   對於比較簡單的錯誤,直接返回表示錯誤原因的 HRESULT。比如下面幾個

    關於C++異常處理和win32結構異常處理

    2011-09-26 周海漢 abloz.com 2011-09-26 C++中經常會用到try…catch()結構來進行異常處理。但windows平臺VS2005之後,預設的配置,try catch是抓不到硬體異常的,如訪問

    C++Windows 32 結構異常

    C++的標準異常語句try catch只能捕獲c++自身的異常。也就是throw 語句丟擲的異常(也叫軟異常) 而專案中很多地方出錯的原因是由於空指標訪問,除0,或者堆疊溢位所導致的。(通常是Win32 系統異常,可能是軟異常也可能是贏異常) 這就需要有程式碼能捕獲這些異常

    結構異常處理 結構異常處理 的區別

    簡單來說, 結構化異常處理是指使用包含異常的控制結構、隔離的程式碼塊和篩選器來建立異常處理機制。  這樣,您的程式碼可以區分不同型別的錯誤,並根據環境做出相應的響應。  在非結構化異常處理中,位於程式碼開頭的 On Error 語句處理所有異常。 -------

    Windows 結構異常 C / C++異常的一些事

    一直以來對“異常”以及相關的知識很模糊,這次做個整理,回答幾個基本問題以幫助對“異常”的理解: C語言的異常C語言版的try/catch: setjmp, longjmp C++的異常(try/catch)Windows 結構化異常,以及與C++標準異常的聯絡(__try

    解析結構異常處理(SEH)第二部分

    ont .com 裏的 之間 地址 htm 完全 href 執行 書接上一篇:http://www.cnblogs.com/ONDragon/p/6855174.html 雖然這個異常回調機制很好,但它並不是一個完美的解決方案。對於稍微復雜一些的應用程序來說,僅用

    Windows結構異常處理淺析

    null 崩潰 plc 處理程序 了解 got AC doc pdo 近期一直被一個問題所困擾,就是寫出來的程序老是出現無故崩潰,有的地方自己知道可能有問題,但是有的地方又根本沒辦法知道有什麽問題。更苦逼的事情是,我們的程序是需要7x24服務客戶,雖然不需要實時精準零差錯,

    Windows結構異常處理(SEH) - by Matt Pietrek

    原文題目: A Crash Course on the Depths of Win32™ Structured Exception Handling 作者: Matt Pietrek   About Matt Pietrek Matt Pietrek (

    深入解析結構異常處理(SEH)

    這個異常處理回撥函 數,同樣被稱為_except_handler,卻與前面的那個截然不同。它首先打印出ExceptionRecord結構中的異常程式碼和標誌,這個結構 的地址是作為一個指標引數被這個函式接收的。打印出異常標誌的原因一會兒就清楚了。因為_except_handler函式並沒有打算修復出錯的程式碼

    一元線性迴歸模型最小乘法及其C++實現

            監督學習中,如果預測的變數是離散的,我們稱其為分類(如決策樹,支援向量機等),如果預測的變數是連續的,我們稱其為迴歸。迴歸分析中,如果只包括一個自變數和一個因變數,且二者的關係可用一條直線近似表示,這種迴歸分析稱為一元線性迴歸分析。如果迴歸分

    【深度學習框架Caffe學習應用】第四課 Caffe視覺工具

    1.首先準備pycaffe環境 輸入一下命令: 2.網路視覺化的工具 2.1在caffe中,有一個專門用於畫網路結構圖的py檔案:caffe/tools/draw_net.py 2

    漫談相容核心十四:Windows的結構異常處理(一)

    結構化異常處理(Structured Exception Handling),簡稱SEH,是Windows作業系統的一個重要組成部分。 在ReactOS核心的原始碼中,特別是在實現系統呼叫的程式碼中,讀者已經看到很多類似於這樣的程式碼:    if(MaximumSize

    Win32結構異常處理(SEH)——異常處理程式(__try/__except)

    20:       {21:           a = 2;0040D8C3   mov         dword ptr [ebp-1Ch],222:       }0040D8CA   mov         dword ptr [ebp-4],0FFFFFFFFh23:       a = 3;00

    2Python3筆記 數據類型"組"(序列) 集合

    屬於 name col set 集合 列表 註意 ack 符號   一、 list [ 列表 ] ---- 任意數據類型的有序排列     1. list 操作類似 str操作 (參考上一節字符串操作)       [1,2,3,4]       [1,2,‘a‘, Tru

    Windows核心程式設計筆記十八 SEH結構異常

    23.2 編譯器層面對系統SEH機制的封裝 23.2.1 擴充套件的EXCEPTION_REGISTRATION級相關結構:VC_EXCEPTION_REGISTRATION (1)VC_EXCEPTION_REGISTRATION結構 struct VC_EX

    Java 中的異常處理機制的簡單原理應用

    異常是指 java 程式執行時(非編譯)所發生的非正常情況或錯誤,與現實生活中的事件很 相似,現實生活中的事件可以包含事件發生的時間、地點、人物、情節等資訊,可以用一個 物件來表示,Java 使用面向物件的方式來處理異常,它把程式中發生的每個異常也都分別封 裝到一個物件來表示

    Java基礎:深入理解java異常處理機制原理和開發應用【轉載】

    Java異常處理機制在日常開發中應用頻繁,本篇文章主要在基礎的使用方法上,更進一步的,如何更加合理的使用異常機制,希望可以對各位朋友能有所幫助。   Java異常處理機制其最主要的幾個關鍵字:try、catch、finally、throw、throws,以及各種各樣

    C語言中的異常處理機制

    軟件測試 如何實現 char* oar 朋友 核心 初始化 flag out #define try if(!setjmp(Jump_Buffer)) 返回try現場後重新執行判斷,所以有兩次執行。 http://blog.csdn.net/tian_dao_chou_q

    C#進階--WebApi異常處理機制

    str with 分配 客戶 來看 stack 統一 releases 全局配置 其實對於C#異常處理大家都不陌生,但是對於在WeiApi上的異常處理實際上也和傳統異常處理區別不大,但是卻經過封裝可以讓異常更加友好,https://docs