逆向-函數

函數的工作機制主要是依託棧結構來實現的,首先棧的一種後進先出的結構,爲什麼使用這種數據結構,因爲這和我們函數調用的流程很類似,當程序嵌套調用時,最後一個調用的函數總是最先返回。

對於函數,我們先從宏觀上大體的先去了解,後面在看一些內部細節,簡單理解函數調用的話,就是在程序動態的運行中,每進入一個函數,總有一塊獨立的棧函數空間供它使用,這段空間可以存儲各類函數需要用到信息,如局部變量,參數等等,當函數返回時,該空間會銷燬。

那麼說到棧,當有函數調用發生,需要開闢一塊新的棧空間,這時總得記錄棧頂的位置吧,只有知道了棧頂的位置,這樣子我們才能擡高棧頂(開闢新空間),相對於棧頂,還有一個概念就是棧底,也可以說是函數的底部位置。對於棧頂和棧底,CPU中使用ESP和EBP這兩個寄存器來保存其內容。

ESP指向棧頂
    //函數的內部空間
EBP指向棧底

下面來看一段遞歸程序,用於分析其函數調用的過程

int GetSum(int num)
{
    num && (num += GetSum(num-1)); //num爲0時不會執行後半部分,遞歸出口
    return num;
}

int main(int argc, char* argv[])
{
    printf("%d",GetSum(2));
    return 0;
}

下面來畫一下函數遞歸調用的流程圖

函數的返回流程圖

所以這裏最終的打印結果就是3。看完上面的流程分析,那麼現在對函數的棧結構應該有一定的宏觀上的瞭解了。

下面就可以說說細節了,我們就來分析一下上述程序的彙編代碼,看懂彙編後,你就能知道程序是如何轉移的,又是如何返回的,又是如何獲取參數的...

先來看Main函數中的調用

12:       printf("%d",GetSum(2));
00401158 6A 02                push        2   //壓入參數
0040115A E8 BF FE FF FF       call        @ILT+25(sub_4010A0) (0040101e)  //調用GetSum
0040115F 83 C4 04             add         esp,4

對於call指令而言,其實他的工作就是把下一行的彙編指令地址給壓入棧中,也就是說我們可以如下等價替換

12:       printf("%d",GetSum(2));
00401158 6A 02                push        2
                              push 0040115F //壓入下一行的地址
                              mov EIP,0040101e //修改EIP使其到跳轉到函數(模擬,真實該指令無效)
0040115F 83 C4 04             add         esp,4

那麼爲什麼會需要壓入下一行的地址呢?這裏主要是用於函數返回時用的,當你已經進入到被調用的函數內部,那麼就無法知道函數需要返回到哪裏了,所以需要先將函數的下一行彙編地址記錄下來,當函數返回時,讀取該地址以便於繼續執行。

下面開始分析函數內部的實現

4:    int GetSum(int num)
5:    {
00401030 55                   push        ebp      //保存棧底指針
00401031 8B EC                mov         ebp,esp  //調整當前棧底指針到棧頂
00401033 83 EC 40             sub         esp,40h  //擡高棧頂,開闢空間
00401036 53                   push        ebx  //保存環境 ebx esi edi
00401037 56                   push        esi
00401038 57                   push        edi
00401039 8D 7D C0             lea         edi,[ebp-40h]
0040103C B9 10 00 00 00       mov         ecx,10h
00401041 B8 CC CC CC CC       mov         eax,0CCCCCCCCh
00401046 F3 AB                rep stos    dword ptr [edi]  //debug版會將開闢的空間填充0xcc
6:        num && (num += GetSum(num-1)); //num爲0時不會執行後半部分,遞歸出口
00401048 83 7D 08 00          cmp         dword ptr [ebp+8],0
0040104C 74 17                je          GetSum+35h (00401065)  //爲零則遞歸結束,直接跳到返回
0040104E 8B 45 08             mov         eax,dword ptr [ebp+8] //使用ebp+xx獲取參數
00401051 83 E8 01             sub         eax,1
00401054 50                   push        eax  //num-1後當做參數壓入
00401055 E8 C4 FF FF FF       call        @ILT+25(sub_4010A0) (0040101e) //遞歸調用
0040105A 83 C4 04             add         esp,4
0040105D 8B 4D 08             mov         ecx,dword ptr [ebp+8]  //重新獲取參數num
00401060 03 C8                add         ecx,eax  //將num加上面函數的返回值
00401062 89 4D 08             mov         dword ptr [ebp+8],ecx
7:        return num;
00401065 8B 45 08             mov         eax,dword ptr [ebp+8]  //eax存放返回結果
8:    }
00401068 5F                   pop         edi  //還原環境 edi esi ebx
00401069 5E                   pop         esi
0040106A 5B                   pop         ebx
0040106B 83 C4 40             add         esp,40h  //降低棧頂,釋放空間
0040106E 3B EC                cmp         ebp,esp
00401070 E8 FB 04 00 00       call        __chkesp (00401570)  //debug下的堆棧平衡檢查
00401075 8B E5                mov         esp,ebp  //還原esp
00401077 5D                   pop         ebp  //還原原先函數的棧底指針
00401078 C3                   ret             //返回函數

基本上對彙編指令的理解都寫在上面了,下面我們還是來畫一下其中的流程圖,重心放在ESP和EBP如何變化

OK,看完上面的流程圖,也就可以明白了,此時EBP+4的位置就是返回地址,而+8的地方就是參數了。而對於函數內部的局部變量,則使用EBP-xxx來訪問,因爲0x40開闢的空間就是用於存放局部變量。當內部的那個函數調用返回時,此時又會回到平衡的狀態(保存環境後)。

下面再來看一下返回時的流程圖

對於上面的流程圖來說,主要需要解釋一下ret指令,對於ret指令,其可以等價替換爲

pop EIP
    也就是將此時ESP指向的內存的值給EIP,其到返回繼續執行流程的效果

而對於後面 add esp,4的指令,說明這裏的平衡堆棧是調用方來平衡,至於如何平衡,這裏就是調用約定的問題了,後面在來細說。到這裏,函數的調用過程應該十分清晰了,下面來整理一下調用過程

1. 按調用約定傳遞參數
2. 保存函數返回的地址
3. 流程轉移到被調用方的函數首地址
4. 保存調用方的棧底(棧底穩定)
5. 以當前棧頂作爲被調用方的棧底
6. 爲局部變量分配空間
7. 保存寄存器環境
8. 可選項,局部變量初始化爲0xcc (debug)
9. 執行被調方的函數體
10.恢復處理器環境
11.釋放局部變量空間
12.恢復調用方的棧底
13.彈出當前棧頂的值作爲返回的代碼流程地址
14.此時流程回到調用方

好了,下面開始說調用約定的事,對於調用約定的問題,從名字上就可以看出來,既然是約定,那麼相當於是一種規則,也就是調用方和被調用方雙方約定的規則,這個規則主要體現在兩個方面

1.參數如何傳遞
2.函數棧誰來平衡(參數的平衡)
    當參數使用棧空間進行傳遞時,這部分空間在函數調用結束後需要平衡

下面就來先看一下下面三個調用約定

cdecl - C約定
stdcall - 標準約定,跨平臺
fastcall - 非標準,微軟

注意,這裏除了上面的三個調用約定外,還有很多調用約定,對於不同的編譯器會有自己的調用約定。對於其他的調用約定,我們只需把握好上面說的兩個規則就好。

先來看,cdecl約定,也就是常說的C約定,這也是默認的約定(函數不加約定時)

1.參數從右到左保存(push)
2.由調用方平棧

下面來看一個例子

int __cdecl fun(int a,int b,int c)
{
    return a * b / c;
}

int main(int argc, char* argv[])
{
	return fun(argc,1,2);
}

分析反彙編代碼

//調用處彙編代碼
00401078 6A 02                push        2
0040107A 6A 01                push        1
0040107C 8B 45 08             mov         eax,dword ptr [ebp+8]
0040107F 50                   push        eax  //參數右到左壓入
00401080 E8 8A FF FF FF       call        @ILT+10(fun) (0040100f)
00401085 83 C4 0C             add         esp,0Ch  //調用方來平衡堆棧

上面的分析完了其實也就可以明白了,對於最開始的遞歸程序例子,使用的就是c約定。下面再來看一下stdcall

1.參數從右到左保存(push)
2.由被調用方平棧

將上例的例子中的cdecl修改爲stdcall後觀察其彙編代碼

//調用處彙編代碼
00401078 6A 02                push        2
0040107A 6A 01                push        1
0040107C 8B 45 08             mov         eax,dword ptr [ebp+8]
0040107F 50                   push        eax  //參數從右到左壓棧
00401080 E8 80 FF FF FF       call        @ILT+0(_fun@12) (00401005)
//此時後面無平棧彙編代碼


//函數彙編代碼
7:    int __stdcall fun(int a,int b,int c)
8:    {
00401020 55                   push        ebp
00401021 8B EC                mov         ebp,esp
00401023 83 EC 40             sub         esp,40h
00401026 53                   push        ebx
00401027 56                   push        esi
00401028 57                   push        edi
00401029 8D 7D C0             lea         edi,[ebp-40h]
0040102C B9 10 00 00 00       mov         ecx,10h
00401031 B8 CC CC CC CC       mov         eax,0CCCCCCCCh
00401036 F3 AB                rep stos    dword ptr [edi]
9:        return a * b / c;
00401038 8B 45 08             mov         eax,dword ptr [ebp+8]
0040103B 0F AF 45 0C          imul        eax,dword ptr [ebp+0Ch]
0040103F 99                   cdq
00401040 F7 7D 10             idiv        eax,dword ptr [ebp+10h]
10:   }
00401043 5F                   pop         edi
00401044 5E                   pop         esi
00401045 5B                   pop         ebx
00401046 8B E5                mov         esp,ebp
00401048 5D                   pop         ebp
00401049 C2 0C 00             ret         0Ch  //注意這裏 ret n,
//此時在pop eip後還在做平衡堆棧的操作,0ch就是平衡的字節數,相當於 sub esp,0ch

從彙編可以看出來,stdcall相當於C約定,區別在於誰來平衡堆棧,而從函數內部的平衡堆棧的字節數,也可以反推出其參數的個數,因爲壓入總是四字節的,所以0xC除以4,那麼就是3,說明此時有3個參數(嚴格來說只能判斷函數調用時有幾個push,畢竟有些參數的大小可能會大於四字節)。

下面再來看一下fastcall,這個約定是非標準的,也就是說是屬於和編譯器綁定的,這個是vs系列的編譯器。

1.前兩參數會優先使用ecx和edx進行傳參,不夠時使用棧傳參(順序還是右到左)
2.由被調用方平棧
//函數調用處彙編代碼
00401088 6A 02                push        2  //參數三使用棧傳遞
0040108A BA 01 00 00 00       mov         edx,1 //參數二使用edx寄存器
0040108F 8B 4D 08             mov         ecx,dword ptr [ebp+8] //參數一使用ecx寄存器
00401092 E8 6E FF FF FF       call        @ILT+0(@fun@12) (00401005)


//函數
7:    int __fastcall fun(int a,int b,int c)
8:    {
00401020 55                   push        ebp
00401021 8B EC                mov         ebp,esp
00401023 83 EC 48             sub         esp,48h
00401026 53                   push        ebx
00401027 56                   push        esi
00401028 57                   push        edi
00401029 51                   push        ecx //保存參數ecx
0040102A 8D 7D B8             lea         edi,[ebp-48h]
0040102D B9 12 00 00 00       mov         ecx,12h
00401032 B8 CC CC CC CC       mov         eax,0CCCCCCCCh
00401037 F3 AB                rep stos    dword ptr [edi]
00401039 59                   pop         ecx //還原參數ecx
0040103A 89 55 F8             mov         dword ptr [ebp-8],edx  //在函數內部無對edx賦值就直接使用說明是外部傳遞的
0040103D 89 4D FC             mov         dword ptr [ebp-4],ecx //這裏雖然上方有使用,但是push和pop還原相當於直接使用外部傳遞
9:        return a * b / c;
00401040 8B 45 FC             mov         eax,dword ptr [ebp-4]
00401043 0F AF 45 F8          imul        eax,dword ptr [ebp-8]
00401047 99                   cdq
00401048 F7 7D 08             idiv        eax,dword ptr [ebp+8]
10:   }
0040104B 5F                   pop         edi
0040104C 5E                   pop         esi
0040104D 5B                   pop         ebx
0040104E 8B E5                mov         esp,ebp
00401050 5D                   pop         ebp
00401051 C2 04 00             ret         4   //內平棧,此時只需平衡一個參數的空間大小即可,因爲另外兩個使用寄存器

可以看出來,使用fastcall約定時,由於使用的是寄存器傳參,其速度也會比另外兩個快那麼一點點。

最後需要注意的是,對於調用約定的判斷,千萬不能從調用函數處判斷,如c約定來說,雖然其特徵都是在調用處,但是我們推斷參數的個數時還是需要從函數的內部進行判斷,看函數內有對參數的引用的個數(ebp+xxx),以及最後有沒有平棧的動作。因爲在函數調用處判斷約定很容易看走眼,特別在release版中有流水線優化時其更容易分析錯誤,所以需要從函數內部入手去判斷調用約定。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章