逆向脫殼分析基礎學習筆記九 C語言內聯彙編和三種呼叫協定 裸函式

語言: CN / TW / HK

本文為本人在 大神論壇 學習逆向破解脫殼學習筆記之一,為本人對以往所學的回顧和總結,可能會有謬誤之處,歡迎大家指出。 陸續將不斷有筆記放出,希望能對想要入門的萌新有所幫助,一起進步

C語言內聯彙編和呼叫協定

前面我們通過分析反彙編分析了C語言,現在我們來探究如何在C語言裡直接自寫彙編函式

這裡就要引入C語言中裸函式的概念

裸函式

宣告裸函式

裸函式與普通函式的區別在於在函式前多聲明瞭

__declspec (naked)

以此來表面該函式是一個裸函式

裸函式作用

要講裸函式的作用,就不得不提到裸函式與普通函式的區別

裸函式與普通函式區別

前面反彙編C語言的筆記裡,我們可以得知一個普通空函式的反彙編程式碼並不少,保護現場、恢復現場等等都有,那麼這些反彙編程式碼是如何產生的呢?

答案是:編譯器

編譯器會為我們產生這些反彙編程式碼

相比之下,只要普通函式加上裸函式字首轉化為裸函式,編譯器就會知道這個函式無需額外生成上面所說的保護現場、恢復現場等反彙編程式碼,函式執行所需的反彙編程式碼由我們自己來實現

於是裸函式的作用呼之欲出:

當我們不希望編譯器為我們生成函式裡的彙編程式碼,而是想要自己實現函式內部的彙編程式碼時,就可以使用裸函式來告訴編譯器不要去額外生成彙編程式碼

最簡單的裸函式

void __declspec (naked) function(){
        __asm{
                ret
        }
}

上面是一個最簡單的裸函式,反彙編程式碼只有一行ret

與普通空函式相比,同樣是什麼都沒做,但卻要加上ret,為什麼?

我們可以來看看這個最簡單的裸函式的執行流程,並注意與普通空函式對比

分析最簡單的裸函式

完整程式碼

先給出完整的程式碼

#include "stdafx.h"

void __declspec (naked) function(){
        __asm{
                ret
        }
}
int main(int argc, char* argv[])
{
        function();
        return 0;
}

接下來進入反彙編的世界

函式外部

13:       function();
00401078   call        @ILT+10(function) (0040100f)
14:       return 0;
0040107D   xor         eax,eax
15:   }
0040107F   pop         edi
00401080   pop         esi
00401081   pop         ebx
00401082   add         esp,40h
00401085   cmp         ebp,esp
00401087   call        __chkesp (004010e0)
0040108C   mov         esp,ebp
0040108E   pop         ebp
0040108F   ret

函式內部

6:    void __declspec (naked) function(){
00401030   ret

開始分析

00401078   call        @ILT+10(function) (0040100f)

我們可以發現裸函式的呼叫和普通函式的呼叫並沒有什麼區別,都是call 函式地址

但接著進入函式內部就可以看到

00401030   ret

函式的內部有且僅有我們程式碼中用__asm所寫的ret語句,沒有任何其餘的程式碼

執行完ret語句後,函式正常返回,接下來就和普通的空函式沒有區別了

得出結論

於是我們可以知道,裸函式的內部彙編程式碼完全由我們自己來實現,之所以要寫上一個ret也是為了讓函式能夠正常地返回

再來看看不新增ret語句時的反彙編程式碼情況

我們將裸函式內部的__asm刪除

void __declspec (naked) function(){

}

然後再觀察函式內部的彙編程式碼

我們可以看到函式內部為一堆 int 3,也就是CC即初始化堆疊的內容

程式執行到這裡就會產生中斷,無法正常執行,所以我們才要加上一條彙編ret語句,讓函式能夠正常執行返回

大致瞭解了裸函式後,我們就來使用內聯彙編自己實現加法函式

內聯彙編實現加法函式

自寫加法函式

#include "stdafx.h"

int __declspec (naked) Plus(int x,int y){
                __asm{
                //保留呼叫前堆疊
                push ebp
                //提升堆疊
                mov ebp,esp
                sub esp,0x40
                //保護現場
                push ebx
                push esi
                push edi
                //初始化提升的堆疊,填充緩衝區
                mov eax,0xCCCCCCCC
                mov ecx,0x10
                lea edi,dword ptr ds:[ebp-0x40]
                rep stosd
                //函式核心功能

                //取出引數
                mov eax,dword ptr ds:[ebp+8]
                //引數相加
                add eax,dword ptr ds:[ebp+0xC]

                //恢復現場
                pop edi
                pop esi
                pop ebx

                //降低堆疊
                mov esp,ebp
                pop ebp                

                //返回
                ret 
        }        
}
int main(int argc, char* argv[])
{
        Plus(1,2);
        return 0;
}

不難發現,其實我們自己實現的加法函式就是模擬了編譯器為我們做的事情,此時進到函式內部也會看到

函式內部

6:    int __declspec (naked) Plus(int x,int y){
00401030   push        ebp
7:        __asm{
8:            //保留呼叫前堆疊
9:            push ebp
10:           //提升堆疊
11:           mov ebp,esp
00401031   mov         ebp,esp
12:           sub esp,0x40
00401033   sub         esp,40h
13:           //保護現場
14:           push ebx
00401036   push        ebx
15:           push esi
00401037   push        esi
16:           push edi
00401038   push        edi
17:           //初始化提升的堆疊,填充緩衝區
18:           mov eax,0xCCCCCCCC
00401039   mov         eax,0CCCCCCCCh
19:           mov ecx,0x10
0040103E   mov         ecx,10h
20:           lea edi,dword ptr ds:[ebp-0x40]
00401043   lea         edi,ds:[ebp-40h]
21:           rep stosd
00401047   rep stos    dword ptr [edi]
22:           //函式核心功能
23:
24:           //取出引數
25:           mov eax,dword ptr ds:[ebp+8]
00401049   mov         eax,dword ptr ds:[ebp+8]
26:           //引數相加
27:           add eax,dword ptr ds:[ebp+0xC]
0040104D   add         eax,dword ptr ds:[ebp+0Ch]
28:
29:
30:           //恢復現場
31:           pop edi
00401051   pop         edi
32:           pop esi
00401052   pop         esi
33:           pop esi
00401053   pop         esi
34:
35:           //降低堆疊
36:           mov esp,ebp
00401054   mov         esp,ebp
37:           pop ebp
00401056   pop         ebp
38:
39:           //返回
40:           ret

執行的就是我們自己所寫的程式碼,而非編譯器所生成的,並且也能夠實現加法函式的功能

函式返回後

逆向脫殼分析基礎學習筆記九 C語言內聯彙編和三種呼叫協定 裸函式6

我們可以發現函式返回後和普通函式並無差異

00401081   add         esp,8

都有這一行平衡堆疊的語句,也就是堆疊外平衡,但如果我們想要在函式內部就平衡堆疊,也就是實現堆疊內平衡,也就是希望函式返回後沒有這個外部的堆疊平衡語句,讓堆疊的平衡工作由我們自己來處理,該如何做到?

這裡就要引入C語言的呼叫協定這個概念了

呼叫協定

常見的幾種呼叫協定:

逆向脫殼分析基礎學習筆記九 C語言內聯彙編和三種呼叫協定 裸函式7

其中__cdecl為C語言預設呼叫協定

接下來我們來比較一下這三種呼叫協定

int __cdecl Plus1(int x,int y){
        return x+y;
}
int __stdcall Plus2(int x,int y){
        return x+y;
}
int __fastcall Plus3(int x,int y){
        return x+y;
}

同樣都是一個簡單的加法函式,分別採用了三種不同的呼叫協定,我們來用匯編來一察他們的區別

__cdecl

首先是我們最熟悉的__cdecl協定,和我們上一個筆記分析的簡單加法函式不無區別

觀察反彙編:

函式外部

逆向脫殼分析基礎學習筆記九 C語言內聯彙編和三種呼叫協定 裸函式8

函式內部

逆向脫殼分析基礎學習筆記九 C語言內聯彙編和三種呼叫協定 裸函式9

我們這裡主要是關注三個地方:引數壓棧、函式返回值、返回後執行語句

引數壓棧:先push 2再 push 1

函式返回值:ret

返回後執行語句:add esp,8

__stdcall

函式外部

逆向脫殼分析基礎學習筆記九 C語言內聯彙編和三種呼叫協定 裸函式10

函式內部

逆向脫殼分析基礎學習筆記九 C語言內聯彙編和三種呼叫協定 裸函式11

接著關注三個地方:引數壓棧、函式返回值、返回後執行語句

引數壓棧:先push 2再 push 1

函式返回值:ret 8

返回後執行語句:xor eax,eax

__fastcall

函式外部

逆向脫殼分析基礎學習筆記九 C語言內聯彙編和三種呼叫協定 裸函式12

函式內部

逆向脫殼分析基礎學習筆記九 C語言內聯彙編和三種呼叫協定 裸函式13

依舊關注三個地方:引數壓棧、函式返回值、返回後執行語句

引數壓棧:先move edx,2再mov ecx,1

函式返回值:ret

返回後執行語句:xor eax,eax

對比三種協定

逆向脫殼分析基礎學習筆記九 C語言內聯彙編和三種呼叫協定 裸函式14

我們可以得出結論:

__cdecl是將引數壓入棧中,然後在函式執行返回後再平衡堆疊,也就是堆疊外平衡

__stdcall也是將引數壓入棧中,但是是在函式內部通過ret xxx來平衡堆疊,也就是堆疊內平衡

__fastcall則是在引數個數小於等於2時直接使用edx和ecx作為引數傳遞的載體,沒用使用到堆疊,自然也就無須平衡堆疊,但是當引數個數大於2時,則多出來的那幾個引數則按stdcall的方式來處理,也是採用堆疊內平衡

接下來再談談__stdcall中返回值的問題

我們可以看到,我們在上面的加法函式中push了兩個立即數2和1,返回值是8

這是不是意味著ret xxxx中xxxx=引數個數*4?

並不是!!!這裡ret xxxx裡的xxxx和壓入引數的資料寬度有關

我們這裡壓入的兩個立即數的資料寬度都是4個位元組=32bit,因此我們這裡是ret 4+4=8

如果改成push ax,也就是壓入2個位元組=16bit時則應該ret 2

這裡可以參考我之前發表的堆疊相關彙編指令的push指令

瞭解了以上呼叫協定後,我們就可以修改之前的簡單加法裸函式,將其改為堆疊內平衡

堆疊內平衡加法函式

__declspec (naked) __stdcall int  Plus(int x,int y){
                __asm{
                //保留呼叫前堆疊
                push ebp
                //提升堆疊
                mov ebp,esp
                sub esp,0x40
                //保護現場
                push ebx
                push esi
                push edi
                //初始化提升的堆疊,填充緩衝區
                mov eax,0xCCCCCCCC
                mov ecx,0x10
                lea edi,dword ptr ds:[ebp-0x40]
                rep stosd
                //函式核心功能

                //取出引數
                mov eax,dword ptr ds:[ebp+8]
                //引數相加
                add eax,dword ptr ds:[ebp+0xC]
                //恢復現場
                pop edi
                pop esi
                pop ebx                
                //降低堆疊
                mov esp,ebp
                pop ebp

                //返回
                ret 8
        }        
}
int main(int argc, char* argv[])
{
        Plus(1,2);
        return 0;
}

與前面相比,修改ret 為ret 8,自己在函式內實現了堆疊內平衡

本系列逆向脫殼基礎學習都在下方連結中,歡迎下載並交流溝通

大神論壇 逆向脫殼分析基礎學習筆記九 C語言內聯彙編和... - 『學習資料區』 - 大神論壇 |脫殼破解|易語言|病毒分析|www.dslt.tech

版權宣告:本文由 lyl610abc 原創,歡迎分享本文,轉載請保留出處

「其他文章」