x64 番外篇——知識鋪墊

寂靜的羽夏發表於2022-03-30

寫在前面

  此係列是本人一個字一個字碼出來的,包括示例和實驗截圖。由於系統核心的複雜性,故可能有錯誤或者不全面的地方,如有錯誤,歡迎批評指正,本教程將會長期更新。 如有好的建議,歡迎反饋。碼字不易,如果本篇文章有幫助你的,如有閒錢,可以打賞支援我的創作。如想轉載,請把我的轉載資訊附在文章後面,並宣告我的個人資訊和本人部落格地址即可,但必須事先通知我

你如果是從中間插過來看的,請仔細閱讀 羽夏看Win系統核心——簡述 ,方便學習本教程。

  看此教程之前,問幾個問題,基礎知識儲備好了嗎?保護模式篇學會了嗎?練習做完了嗎?沒有的話就不要繼續了。


? 華麗的分割線 ?


簡述

  初入64位的核心世界,64位的彙編肯定是基礎。在64位的Win作業系統,呼叫約定並不是原來的多種多樣,而是隻有一種呼叫約定FastCall。並且在64位下,作業系統以及應用程式十分注重對齊(地址數值可以被16整除)和棧幀這個事情,並且SEH的實現也不再基於堆疊,這一切將在本篇我會詳細介紹。
  本部分討論的x64AMD64Intel64的合稱,是指與現有x86相容的64位CPU。在64位系統中,記憶體地址為64位。64位環境下暫存器有比較大的變化,如下圖所示:

x64 番外篇——知識鋪墊

x64 番外篇——知識鋪墊

  在介紹本節東西之前,我們先學習在64位下的僅有FastCall呼叫約定,實行外平棧:

引數 型別 浮點型別
第1個引數 RCX XMM0
第2個引數 RDX XMMI
第3個引數 R8 XMM2
第4個引數 R9 XMM3

  瞭解這些東西之後,我們接下來對64位的彙編進行鋪墊。

彙編鋪墊

  當我們初步踏入64位彙編的世界時,我們先看看我們入門 羽夏看C語言 系列教程的時候會提供一個最簡單的示例來從彙編角度來看C/C++,現在我們重新用64位來看看它們現在的樣子,如下是示例程式碼:

#include <iostream>

using namespace std;

int main()
{
    int a = 1;
    cout << a << endl;
    return 0;
}

  它的反彙編如下所示:

#include <iostream>

using namespace std;

int main()
{
00007FF628591860  push        rbp  
00007FF628591862  push        rdi  
00007FF628591863  sub         rsp,108h  
00007FF62859186A  lea         rbp,[rsp+20h]  
    int a = 1;
00007FF62859186F  mov         dword ptr [a],1  
    cout << a << endl;
00007FF628591876  mov         edx,dword ptr [a]  
00007FF628591879  mov         rcx,qword ptr [__imp_std::cout (07FF6285A0170h)]  
00007FF628591880  call        qword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (07FF6285A0158h)]  
00007FF628591886  lea         rdx,[std::endl<char,std::char_traits<char> > (07FF628591037h)]  
00007FF62859188D  mov         rcx,rax  
00007FF628591890  call        qword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (07FF6285A0150h)]  
    return 0;
00007FF628591896  xor         eax,eax  
}
00007FF628591898  lea         rsp,[rbp+0E8h]  
00007FF62859189F  pop         rdi  
00007FF6285918A0  pop         rbp  
00007FF6285918A1  ret  

  可以看出,彙編似乎沒有太大的變化,依舊採用rbp定址,但是這個定址看起來比較奇怪,下面我來逐步介紹這些奇怪之處。
  lea rbp,[rsp+20h]這句彙編程式碼看起來比較奇怪,其實這裡是預留給引數傳遞的空間,正好是4個引數的空間,在引數不多於4個的時候會採用,一共32個位元組。稍後我們會對此進行展開。
  還有一個比較奇怪的點,如下所示:

00007FF628591863  sub         rsp,108h  
00007FF62859186A  lea         rbp,[rsp+20h]  

……

00007FF628591898  lea         rsp,[rbp+0E8h]  

  看到在恢復堆疊的時候這兩個數值不太一樣了嗎?這就是中間呼叫一些函式進行內平棧的結果,我們函式就寫一個return 0;看看它的反彙編結果:

int main()
{
00007FF704AB1830  push        rbp  
00007FF704AB1832  push        rdi  
00007FF704AB1833  sub         rsp,0C8h  
00007FF704AB183A  mov         rbp,rsp  

    return 0;
00007FF704AB183D  xor         eax,eax  
}
00007FF704AB183F  lea         rsp,[rbp+0C8h]  
00007FF704AB1846  pop         rdi  
00007FF704AB1847  pop         rbp  
00007FF704AB1848  ret  

  這時候提升的堆疊和恢復的堆疊就是一模一樣了。
  下面我們繼續來詳細介紹有關引數呼叫的細節,當我們傳參不多於4個的時候,它是怎樣傳參的,如下是測試程式碼:

#include <iostream>

using namespace std;

int add(int a, int b, int c, int d)
{
    return a + b + c + d;
}

int main()
{
    int a = 3, b = 4, c = 5, d = 6;
    int e = add(a, b, c, d);
    return 0;
}  

  先看add函式的反彙編:

int add(int a, int b, int c, int d)
{
00007FF633681830  mov         dword ptr [rsp+20h],r9d  
00007FF633681835  mov         dword ptr [rsp+18h],r8d  
00007FF63368183A  mov         dword ptr [rsp+10h],edx  
00007FF63368183E  mov         dword ptr [rsp+8],ecx  
00007FF633681842  push        rbp  
00007FF633681843  push        rdi  
00007FF633681844  sub         rsp,0C8h  
00007FF63368184B  mov         rbp,rsp  
    return a + b + c + d;
00007FF63368184E  mov         eax,dword ptr [b]  
00007FF633681854  mov         ecx,dword ptr [a]  
00007FF63368185A  add         ecx,eax  
00007FF63368185C  mov         eax,ecx  
00007FF63368185E  add         eax,dword ptr [c]  
00007FF633681864  add         eax,dword ptr [d]  
}
00007FF63368186A  lea         rsp,[rbp+0C8h]  
00007FF633681871  pop         rdi  
00007FF633681872  pop         rbp  
00007FF633681873  ret  

  對於開頭的彙編程式碼,可能有點難理解:

mov dword ptr [rsp+20h],r9d
mov dword ptr [rsp+18h],r8d
mov dword ptr [rsp+10h],edx
mov dword ptr [rsp+8],ecx  

  如上引數就是儲存在所謂的預留空間,示意圖如下:

x64 番外篇——知識鋪墊

  這預留的棧空間是在主函式內完成的,這個暫且先不關注。後面的程式碼緊接著是經典的rbp定址,但是眼尖的同志可能會發現,後面的運算都是用32位暫存器,沒有用64位的。
  這裡我囉嗦一下,64位暫存器是對32位的擴充套件,但是有些彙編指令32位有但是64位沒有的,我們接下來探究這個事情。
  對32位暫存器的寫操作,包括運算結果,對相應的64位暫存器的高32位清0。這個是64位不同於32位的操作,我們用一個動圖來展示一下該效果:

x64 番外篇——知識鋪墊

  由於32位指令編碼比對應的64位指令編碼指令要短,為了優化就會使用較短的32位指令編碼。比如xor rax,rax這條指令,它的硬編碼為48 33 C0,而xor eax,eax可以實現相同的功能,它的硬編碼為33 C0,那麼編譯器會優先使用xor eax,eax
  有些32位的彙編指令對應64位是沒有的,比如push,在64位是沒有的:

x64 番外篇——知識鋪墊

  記憶體優先使用相對偏移定址,直接定址指令較少。這個我們來看一個例子,如下圖所示:

x64 番外篇——知識鋪墊

  可以看到硬編碼的結果了嗎?接的內容是0,但是指的是下一行地址,和32位下的jmp的硬編碼方式是一樣的。但是如果間接定址的範圍無法表示了,就寫死地址,類似下面的結果:

x64 番外篇——知識鋪墊

  當然,我們可以將間接定址的改為直接定址的,如下圖所示:

x64 番外篇——知識鋪墊

  這裡再擴充套件比較有意思的nop指令,如下圖所示,需要硬編碼進行輸入:

x64 番外篇——知識鋪墊

  有關64位的彙編就介紹這麼多,我們會過來再看看add函式的傳參情況。後面都是我們學過32位的ebp定址都能看懂的程式碼了,接下來看主函式的反彙編:

int main()
{
00007FF6336817A0  push        rbp  
00007FF6336817A2  push        rdi  
00007FF6336817A3  sub         rsp,188h  
00007FF6336817AA  lea         rbp,[rsp+20h]  
    int a = 3, b = 4, c = 5, d = 6;
00007FF6336817AF  mov         dword ptr [rbp+4],3  
00007FF6336817B6  mov         dword ptr [rbp+24h],4  
00007FF6336817BD  mov         dword ptr [rbp+44h],5  
00007FF6336817C4  mov         dword ptr [rbp+64h],6  
    int e = add(a, b, c, d);
00007FF6336817CB  mov         r9d,dword ptr [rbp+64h]  
00007FF6336817CF  mov         r8d,dword ptr [rbp+44h]  
00007FF6336817D3  mov         edx,dword ptr [rbp+24h]  
00007FF6336817D6  mov         ecx,dword ptr [rbp+4]  
00007FF6336817D9  call        00007FF6336813C5  
00007FF6336817DE  mov         dword ptr [rbp+0000000000000084h],eax  
    return 0;
00007FF6336817E4  xor         eax,eax  
}
00007FF6336817E6  lea         rsp,[rbp+0000000000000168h]  
00007FF6336817ED  pop         rdi  
00007FF6336817EE  pop         rbp  
00007FF6336817EF  ret  

  開頭我講了,後面又來了奇怪的區域性變數分配和初始化:

mov dword ptr [rbp+4],3  
mov dword ptr [rbp+24h],4
mov dword ptr [rbp+44h],5
mov dword ptr [rbp+64h],6

  可以看到,每個區域性變數之間差了0x20個位元組,也就是32個位元組,這是為什麼呢?目前暫時搞不清楚為什麼,可能有對齊的意味在這裡。
  下面我們來看看IDA是如何分析這部分程式碼的:

; int __fastcall main()
main proc near

a= dword ptr -16Ch
b= dword ptr -14Ch
c= dword ptr -12Ch
d= dword ptr -10Ch

push    rbp
push    rdi
sub     rsp, 188h
lea     rbp, [rsp+20h]
mov     [rbp+170h+a], 3
mov     [rbp+170h+b], 4
mov     [rbp+170h+c], 5
mov     [rbp+170h+d], 6
mov     r9d, [rbp+170h+d] ; d
mov     r8d, [rbp+170h+c] ; c
mov     edx, [rbp+170h+b] ; b
mov     ecx, [rbp+170h+a] ; a
call    j_?add@@YAHHHHH@Z ; add(int,int,int,int)
mov     [rbp+84h], eax
xor     eax, eax
lea     rsp, [rbp+168h]
pop     rdi
pop     rbp
retn
main endp

  我們繼續介紹FastCall呼叫約定:pushpop指令僅用來儲存非易變暫存器,其他棧指標操作顯式寫暫存器rsp。實現進入call之前rsp滿足0×10位元組對齊。
通常不使用rbp定址棧記憶體,所以rsp在函式幀中儘量保持穩定,一次性分配區域性變數和引數空間但是。在我們的例項中,用到了rbp定址,但在使用過程中rsp保持比較穩定的狀態。
  上面的介紹僅僅是冰山一角,讓你對64位的彙編指令和呼叫約定有一個整體的認識,具體細節請自行探索。

SEH

概述

  之前我們在32位介紹SEH的時候,它是用棧實現的,但是如果黑客利用構造特殊的程式碼對棧進行攻擊導致程式碼劫持,這是十分不安全的。所以,在64位下,SEH不使用棧來實現。對於64位來說,函式有沒有異常處理程式的執行效率是一樣的,因為它並沒有類似32位掛SEH的操作。我們通過程式碼示例看一下:

#include <iostream>

using namespace std;

int filter()
{
    return 1;
}

int main()
{

    __try
    {
        cout << "try1" << endl;
        __try
        {
            cout << "try2" << endl;
            __try
            {
                cout << "try3" << endl;
            }
            __finally
            {
                cout << "finally" << endl;
            }
        }
        __except (filter())
        {
            cout << "except filter" << endl;
        }
    }
    __except (1)
    {
        cout << "except 1" << endl;
    }

    return 0;
}

  它的反彙編如下:

int main()
{
00007FF72C6222C0  push        rbp  
00007FF72C6222C2  push        rdi  
00007FF72C6222C3  sub         rsp,0E8h  
00007FF72C6222CA  lea         rbp,[rsp+20h]  

    __try
    {
        cout << "try1" << endl;
00007FF72C6222CF  lea         rdx,[string "try1" (07FF72C62AC24h)]  
00007FF72C6222D6  mov         rcx,qword ptr [__imp_std::cout (07FF72C631198h)]  
00007FF72C6222DD  call        std::operator<<<std::char_traits<char> > (07FF72C62108Ch)  
00007FF72C6222E2  lea         rdx,[std::endl<char,std::char_traits<char> > (07FF72C62103Ch)]  
00007FF72C6222E9  mov         rcx,rax  
00007FF72C6222EC  call        qword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (07FF72C6311B0h)]  
00007FF72C6222F2  nop  
        __try
        {
            cout << "try2" << endl;
00007FF72C6222F3  lea         rdx,[string "try2" (07FF72C62AC2Ch)]  
00007FF72C6222FA  mov         rcx,qword ptr [__imp_std::cout (07FF72C631198h)]  
00007FF72C622301  call        std::operator<<<std::char_traits<char> > (07FF72C62108Ch)  
00007FF72C622306  lea         rdx,[std::endl<char,std::char_traits<char> > (07FF72C62103Ch)]  
00007FF72C62230D  mov         rcx,rax  
00007FF72C622310  call        qword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (07FF72C6311B0h)]  
00007FF72C622316  nop  
            __try
            {
                cout << "try3" << endl;
00007FF72C622317  lea         rdx,[string "try3" (07FF72C62AC34h)]  
00007FF72C62231E  mov         rcx,qword ptr [__imp_std::cout (07FF72C631198h)]  
00007FF72C622325  call        std::operator<<<std::char_traits<char> > (07FF72C62108Ch)  
00007FF72C62232A  lea         rdx,[std::endl<char,std::char_traits<char> > (07FF72C62103Ch)]  
00007FF72C622331  mov         rcx,rax  
00007FF72C622334  call        qword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (07FF72C6311B0h)]  
00007FF72C62233A  nop  
            }
            __finally
            {
                cout << "finally" << endl;
00007FF72C62233B  lea         rdx,[string "finally" (07FF72C62AC40h)]  
00007FF72C622342  mov         rcx,qword ptr [__imp_std::cout (07FF72C631198h)]  
00007FF72C622349  call        std::operator<<<std::char_traits<char> > (07FF72C62108Ch)  
00007FF72C62234E  lea         rdx,[std::endl<char,std::char_traits<char> > (07FF72C62103Ch)]  
00007FF72C622355  mov         rcx,rax  
00007FF72C622358  call        qword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (07FF72C6311B0h)]  
            }
        }
00007FF72C62235E  jmp         main+0C4h (07FF72C622384h)  
        __except (filter())
        {
            cout << "except filter" << endl;
00007FF72C622360  lea         rdx,[string "except filter" (07FF72C62AC50h)]  
00007FF72C622367  mov         rcx,qword ptr [__imp_std::cout (07FF72C631198h)]  
00007FF72C62236E  call        std::operator<<<std::char_traits<char> > (07FF72C62108Ch)  
00007FF72C622373  lea         rdx,[std::endl<char,std::char_traits<char> > (07FF72C62103Ch)]  
00007FF72C62237A  mov         rcx,rax  
00007FF72C62237D  call        qword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (07FF72C6311B0h)]  
00007FF72C622383  nop  
        }
    }
00007FF72C622384  jmp         $LN8+24h (07FF72C6223AAh)  
    __except (1)
    {
        cout << "except 1" << endl;
00007FF72C622386  lea         rdx,[string "except 1" (07FF72C62AC60h)]  
00007FF72C62238D  mov         rcx,qword ptr [__imp_std::cout (07FF72C631198h)]  
00007FF72C622394  call        std::operator<<<std::char_traits<char> > (07FF72C62108Ch)  
00007FF72C622399  lea         rdx,[std::endl<char,std::char_traits<char> > (07FF72C62103Ch)]  
00007FF72C6223A0  mov         rcx,rax  
00007FF72C6223A3  call        qword ptr [__imp_std::basic_ostream<char,std::char_traits<char> >::operator<< (07FF72C6311B0h)]  
00007FF72C6223A9  nop  
    }

    return 0;
00007FF72C6223AA  xor         eax,eax  
}
00007FF72C6223AC  lea         rsp,[rbp+0C8h]  
00007FF72C6223B3  pop         rdi  
00007FF72C6223B4  pop         rbp  
00007FF72C6223B5  ret  

  可以看出生成的程式碼和我們認為的普通程式碼沒什麼兩樣,每一個對應的異常處理程式前都會用jmp跳過,感覺十分奇怪。那麼64位是如何實現異常的SEH處理的呢?
  為了方便介紹,我們把編譯後的程式放到IDA裡面,將會得到如下結果:

; int __fastcall main()
main            proc near               ; CODE XREF: j_main↑j
                                        ; DATA XREF: .pdata:000000014001F89C↓o
; __unwind { // j___C_specific_handler_0
                push    rbp
                push    rdi
                sub     rsp, 0E8h
                lea     rbp, [rsp+20h]
                lea     rdx, _Val       ; "try1"
                mov     rcx, cs:__imp_?cout@std@@3V?$basic_ostream@DU?$char_traits@D@std@@@1@A ; _Ostr
                call    j_??$?6U?$char_traits@D@std@@@std@@YAAEAV?$basic_ostream@DU?$char_traits@D@std@@@0@AEAV10@PEBD@Z ; std::operator<<<std::char_traits<char>>(std::ostream &,char const *)
                lea     rdx, j_??$endl@DU?$char_traits@D@std@@@std@@YAAEAV?$basic_ostream@DU?$char_traits@D@std@@@0@AEAV10@@Z ; std::endl<char,std::char_traits<char>>(std::ostream &)
                mov     rcx, rax
                call    cs:__imp_??6?$basic_ostream@DU?$char_traits@D@std@@@std@@QEAAAEAV01@P6AAEAV01@AEAV01@@Z@Z ; std::ostream::operator<<(std::ostream & (*)(std::ostream &))
                nop
                lea     rdx, aTry2      ; "try2"
                mov     rcx, cs:__imp_?cout@std@@3V?$basic_ostream@DU?$char_traits@D@std@@@1@A ; _Ostr
                call    j_??$?6U?$char_traits@D@std@@@std@@YAAEAV?$basic_ostream@DU?$char_traits@D@std@@@0@AEAV10@PEBD@Z ; std::operator<<<std::char_traits<char>>(std::ostream &,char const *)
                lea     rdx, j_??$endl@DU?$char_traits@D@std@@@std@@YAAEAV?$basic_ostream@DU?$char_traits@D@std@@@0@AEAV10@@Z ; std::endl<char,std::char_traits<char>>(std::ostream &)
                mov     rcx, rax
                call    cs:__imp_??6?$basic_ostream@DU?$char_traits@D@std@@@std@@QEAAAEAV01@P6AAEAV01@AEAV01@@Z@Z ; std::ostream::operator<<(std::ostream & (*)(std::ostream &))
                nop
                lea     rdx, aTry3      ; "try3"
                mov     rcx, cs:__imp_?cout@std@@3V?$basic_ostream@DU?$char_traits@D@std@@@1@A ; _Ostr
                call    j_??$?6U?$char_traits@D@std@@@std@@YAAEAV?$basic_ostream@DU?$char_traits@D@std@@@0@AEAV10@PEBD@Z ; std::operator<<<std::char_traits<char>>(std::ostream &,char const *)
                lea     rdx, j_??$endl@DU?$char_traits@D@std@@@std@@YAAEAV?$basic_ostream@DU?$char_traits@D@std@@@0@AEAV10@@Z ; std::endl<char,std::char_traits<char>>(std::ostream &)
                mov     rcx, rax
                call    cs:__imp_??6?$basic_ostream@DU?$char_traits@D@std@@@std@@QEAAAEAV01@P6AAEAV01@AEAV01@@Z@Z ; std::ostream::operator<<(std::ostream & (*)(std::ostream &))
                nop

$LN18:
                lea     rdx, aFinally   ; "finally"
                mov     rcx, cs:__imp_?cout@std@@3V?$basic_ostream@DU?$char_traits@D@std@@@1@A ; _Ostr
                call    j_??$?6U?$char_traits@D@std@@@std@@YAAEAV?$basic_ostream@DU?$char_traits@D@std@@@0@AEAV10@PEBD@Z ; std::operator<<<std::char_traits<char>>(std::ostream &,char const *)
                lea     rdx, j_??$endl@DU?$char_traits@D@std@@@std@@YAAEAV?$basic_ostream@DU?$char_traits@D@std@@@0@AEAV10@@Z ; std::endl<char,std::char_traits<char>>(std::ostream &)
                mov     rcx, rax
                call    cs:__imp_??6?$basic_ostream@DU?$char_traits@D@std@@@std@@QEAAAEAV01@P6AAEAV01@AEAV01@@Z@Z ; std::ostream::operator<<(std::ostream & (*)(std::ostream &))
                jmp     short loc_140012384
; ---------------------------------------------------------------------------

$LN12:
                lea     rdx, aExceptFilter ; "except filter"
                mov     rcx, cs:__imp_?cout@std@@3V?$basic_ostream@DU?$char_traits@D@std@@@1@A ; _Ostr
                call    j_??$?6U?$char_traits@D@std@@@std@@YAAEAV?$basic_ostream@DU?$char_traits@D@std@@@0@AEAV10@PEBD@Z ; std::operator<<<std::char_traits<char>>(std::ostream &,char const *)
                lea     rdx, j_??$endl@DU?$char_traits@D@std@@@std@@YAAEAV?$basic_ostream@DU?$char_traits@D@std@@@0@AEAV10@@Z ; std::endl<char,std::char_traits<char>>(std::ostream &)
                mov     rcx, rax
                call    cs:__imp_??6?$basic_ostream@DU?$char_traits@D@std@@@std@@QEAAAEAV01@P6AAEAV01@AEAV01@@Z@Z ; std::ostream::operator<<(std::ostream & (*)(std::ostream &))
                nop

loc_140012384:                          ; CODE XREF: main+9E↑j
                jmp     short loc_1400123AA
; ---------------------------------------------------------------------------

$LN8:
                lea     rdx, aExcept1   ; "except 1"
                mov     rcx, cs:__imp_?cout@std@@3V?$basic_ostream@DU?$char_traits@D@std@@@1@A ; _Ostr
                call    j_??$?6U?$char_traits@D@std@@@std@@YAAEAV?$basic_ostream@DU?$char_traits@D@std@@@0@AEAV10@PEBD@Z ; std::operator<<<std::char_traits<char>>(std::ostream &,char const *)
                lea     rdx, j_??$endl@DU?$char_traits@D@std@@@std@@YAAEAV?$basic_ostream@DU?$char_traits@D@std@@@0@AEAV10@@Z ; std::endl<char,std::char_traits<char>>(std::ostream &)
                mov     rcx, rax
                call    cs:__imp_??6?$basic_ostream@DU?$char_traits@D@std@@@std@@QEAAAEAV01@P6AAEAV01@AEAV01@@Z@Z ; std::ostream::operator<<(std::ostream & (*)(std::ostream &))
                nop

loc_1400123AA:                          ; CODE XREF: main:loc_140012384↑j
                xor     eax, eax
                lea     rsp, [rbp+0C8h]
                pop     rdi
                pop     rbp
                retn
; } // starts at 1400122C0
main            endp

  有關SEH異常處理的資訊放在了PE結構的Exception目錄,如果對該方面一點不清楚的同志請學習 羽夏筆記——PE結構(不包含.Net) ,否則下面的介紹可能對你來說意義不太大。

RUNTIME_FUNCTION

  在64位下,每一個非葉函式(葉函式就是既不呼叫函式,又沒有修改棧指標,也沒有使用SEH的函式)都有一個結構體來描述該函式的SEH處理資訊,那就是RUNTIME_FUNCTION,它的結構如下:

typedef struct _RUNTIME_FUNCTION {
    ULONG BeginAddress;
    ULONG EndAddress;
    ULONG UnwindData;
} RUNTIME_FUNCTION, *PRUNTIME_FUNCTION;

  第一個成員標誌著開始RVA,第二個成員標誌的是結束RVA。我們來看看main函式的RUNTIME_FUNCTION

RUNTIME_FUNCTION <rva main, rva byte_1400123B6, rva stru_14001C600>

  IDA幫我們給識別好了,我們來看看它的硬編碼:

C0 22 01 00 B6 23 01 00 00 C6 01 00

  為了配合講解,我們把主函式的開始地址和結束地址看一下:

.text:00000001400122C0 ; int __fastcall main()
.text:00000001400122C0 main            proc near               ; CODE XREF: j_main↑j
.text:00000001400122C0                                         ; DATA XREF: .pdata:000000014001F89C↓o
.text:00000001400122C0 ; __unwind { // j___C_specific_handler_0

……

.text:00000001400123B5 main            endp
.text:00000001400123B5
.text:00000001400123B5 ; ---------------------------------------------------------------------------
.text:00000001400123B6 byte_1400123B6  db 3Dh dup(0CCh)        ; DATA XREF: .pdata:000000014001F89C↓o

  也就是說,第一個成員的值就是0x122C0,正好是我們程式的偏移(映象載入的地址為0x140000000),第二個成員的值是0x123B6也就是結束的位置偏移。
  還有一個成員我們並沒有介紹,那就是UnwindData,它其實是一個結構體,裝著異常發生時棧的回滾資訊,如下所示:

typedef struct _UNWIND_INFO {
       UCHAR Version : 3;
       UCHAR Flags : 5;
       UCHAR SizeOfProlog;
       UCHAR CountOfCodes;
       UCHAR FrameRegister : 4;
       UCHAR FrameOffset : 4;
       UNWIND_CODE UnwindCode[1];
   
   //
   // The unwind codes are followed by an optional DWORD aligned field that
   // contains the exception handler address or a function table entry if
   // chained unwind information is specified. If an exception handler address
   // is specified, then it is followed by the language specified exception
   // handler data.
   //
   //  union {
   //      struct {
   //          ULONG ExceptionHandler;
   //          ULONG ExceptionData[];
   //      };
   //
   //      RUNTIME_FUNCTION FunctionEntry;
   //  };
   //
   
   } UNWIND_INFO, *PUNWIND_INFO;

UNWIND_INFO

  該結構前兩個成員是個位域,佔用一個UCHAR大小。第一個成員是版本號,目前都是1,第二個成員是比較重要的成員,它標誌了它的型別,我們來看看:

#define UNW_FLAG_NHANDLER 0x0
#define UNW_FLAG_EHANDLER 0x1
#define UNW_FLAG_UHANDLER 0x2
#define UNW_FLAG_CHAININFO 0x4

  可以看到有四種型別,下面我們來看看它們的含義。

UNW_FLAG_NHANDLER

  表示既沒有EXCEPT_FILTER也沒有EXCEPT_HANDLER,這個是最簡單的型別,它的示意圖如下:

x64 番外篇——知識鋪墊

UNW_FLAG_EHANDLER

   表示該函式有EXCEPT_FILTEREXCEPT_HANDLER,示意圖如下:

x64 番外篇——知識鋪墊

UNW_FLAG_UHANDLER

  表示該函式有FINALLY_HANDLER,它的結構如下:

x64 番外篇——知識鋪墊

UNW_FLAG_CHAININFO

  表示該函式有多個UNWIND_INFO並串接在一起。

SizeOfProlog

  表示該函式的Prolog指令的大小,單位是位元組。

CountOfCodes

  表示當前UNWIND_INFO包含多少個UNWIND_CODE結構。

FrameRegister

   如果函式建立了棧幀,它表示棧幀的索引,否則為0.

FrameOffset

  表示FrameRegister距離函式最初棧頂(剛進入函式,還沒有執行任何指令時的棧頂)的偏移,單位為位元組。

UnwindCode

  是一個UNWIND_CODE型別的不定長陣列,元素數量由CountOfCodes決定。
  這裡在說明幾點:如果Flags設定了UNW_FLAG_EHANDLERUNW_FLAG_UHANDLER,那麼在最後一個UNWIND_CODE之後存放著ExceptionHandler,它相當於 x86的EXCEPTION_REGISTRATION::handle以及ExceptionData它相當於x86的EXCEPTION_REGISTRATION::scopetableUnwindCode陣列詳細記錄了函式修改棧、儲存非易失性暫存器的指令。

UNWIND_CODE

  下面我們來看看UNWIND_CODE結構體:

typedef enum _UNWIND_OP_CODES {
    UWOP_PUSH_NONVOL = 0,
    UWOP_ALLOC_LARGE,       // 1
    UWOP_ALLOC_SMALL,       // 2
    UWOP_SET_FPREG,         // 3
    UWOP_SAVE_NONVOL,       // 4
    UWOP_SAVE_NONVOL_FAR,   // 5
    UWOP_SPARE_CODE1,       // 6
    UWOP_SPARE_CODE2,       // 7
    UWOP_SAVE_XMM128,       // 8
    UWOP_SAVE_XMM128_FAR,   // 9
    UWOP_PUSH_MACHFRAME     // 10
} UNWIND_OP_CODES, *PUNWIND_OP_CODES;

typedef union _UNWIND_CODE {
    struct {
        UCHAR CodeOffset;
        UCHAR UnwindOp : 4;
        UCHAR OpInfo : 4;
    };

    USHORT FrameOffset;
} UNWIND_CODE, *PUNWIND_CODE;

  由於我們這裡是知識鋪墊,具體細節就不去追究了,感興趣的可以自行探索。

下一篇

  x64 番外篇——保護模式相關

相關文章