官术网_书友最值得收藏!

  • 0day安全
  • 王清主編
  • 5185字
  • 2019-01-01 12:26:10

2.1 系統棧的工作原理

2.1.1 內存的不同用途

如果您關注網絡安全問題,那么一定聽過緩沖區溢出這個術語。簡單說來,緩沖區溢出就是在大緩沖區中的數據向小緩沖區復制的過程中,由于沒有注意小緩沖區的邊界,“撐爆”了較小的緩沖區,從而沖掉了和小緩沖區相鄰內存區域的其他數據而引起的內存問題。緩沖溢出是最常見的內存錯誤之一,也是攻擊者入侵系統時所用到的最強大、最經典的一類漏洞利用方式。

成功地利用緩沖區溢出漏洞可以修改內存中變量的值,甚至可以劫持進程,執行惡意代碼,最終獲得主機的控制權。要透徹地理解這種攻擊方式,我們需要回顧一些計算機體系架構方面的基礎知識,搞清楚CPU、寄存器、內存是怎樣協同工作而讓程序流暢執行的。

根據不同的操作系統,一個進程可能被分配到不同的內存區域去執行。但是不管什么樣的操作系統、什么樣的計算機架構,進程使用的內存都可以按照功能大致分成以下4個部分。

(1)代碼區:這個區域存儲著被裝入執行的二進制機器代碼,處理器會到這個區域取指并執行。

(2)數據區:用于存儲全局變量等。

(3)堆區:進程可以在堆區動態地請求一定大小的內存,并在用完之后歸還給堆區。動態分配和回收是堆區的特點。

(4)棧區:用于動態地存儲函數之間的調用關系,以保證被調用函數在返回時恢復到母函數中繼續執行。

題外話:這種簡單的內存劃分方式是為了讓您能夠更容易地理解程序的運行機制。《深入理解計算機系統》一書中有更詳細的關于內存使用的論述,如有興趣可參考之。

在Windows平臺下,高級語言寫出的程序經過編譯鏈接,最終會變成第1章介紹過的PE文件。當PE文件被裝載運行后,就成了所謂的進程。

PE文件代碼段中包含的二進制級別的機器代碼會被裝入內存的代碼區(.text),處理器將到內存的這個區域一條一條地取出指令和操作數,并送入算術邏輯單元進行運算;如果代碼中請求開辟動態內存,則會在內存的堆區分配一塊大小合適的區域返回給代碼區的代碼使用;當函數調用發生時,函數的調用關系等信息會動態地保存在內存的棧區,以供處理器在執行完被調用函數的代碼時,返回母函數。這個協作過程如圖2.1.1所示。

圖2.1.1 進程的內存使用示意圖

如果把計算機看成一個有條不紊的工廠,我們可以得到如下類比。

·CPU是完成工作的工人。

·數據區、堆區、棧區等則是用來存放原料、半成品、成品等各種東西的場所。

·存在代碼區的指令則告訴CPU要做什么,怎么做,到哪里去領原材料,用什么工具來做,做完以后把成品放到哪個貨艙去。

·值得一提的是,棧除了扮演存放原料、半成品的倉庫之外,它還是車間調度主任的辦公室。

程序中所使用的緩沖區可以是堆區、棧區和存放靜態變量的數據區。緩沖區溢出的利用方法和緩沖區到底屬于上面哪個內存區域密不可分,本章主要介紹在系統棧中發生溢出的情形。

2.1.2 棧與系統棧

從計算機科學的角度來看,棧指的是一種數據結構,是一種先進后出的數據表。棧的最常見操作有兩種:壓棧(PUSH)、彈棧(POP);用于標識棧的屬性也有兩個:棧頂(TOP)、棧底(BASE)。

可以把棧想象成一摞撲克牌。

·PUSH:為棧增加一個元素的操作叫做PUSH,相當于在這摞撲克牌的最上面再放上一張。

·POP:從棧中取出一個元素的操作叫做POP,相當于從這摞撲克牌取出最上面的一張。

·TOP:標識棧頂位置,并且是動態變化的。每做一次PUSH操作,它都會自增1;相反,每做一次POP操作,它會自減1。棧頂元素相當于撲克牌最上面一張,只有這張牌的花色是當前可以看到的。

·BASE:標識棧底位置,它記錄著撲克牌最下面一張的位置。BASE用于防止棧空后繼續彈棧(牌發完時就不能再去揭牌了)。很明顯,一般情況下,BASE是不會變動的。內存的棧區實際上指的就是系統棧。系統棧由系統自動維護,它用于實現高級語言中函數的調用。對于類似C語言這樣的高級語言,系統棧的PUSH、POP等堆棧平衡細節是透明的。一般說來,只有在使用匯編語言開發程序的時候,才需要和它直接打交道。

提示:系統棧在其他文獻中可能曾被叫做運行棧、調用棧等。如果不加特別說明,本書中所涉及的棧都是指系統棧這個概念。請您注意將其與編寫非遞歸函數求解“八皇后”問題時,在自己程序中所實現的數據結構區分開來。

2.1.3 函數調用時發生了什么

我們下面就來探究一下高級語言中函數的調用和遞歸等性質是怎樣通過系統棧巧妙實現的。請看如下代碼:

   intfunc_B(int arg_B1, int arg_B2)
{
      int var_B1, var_B2;
      var_B1=arg_B1+arg_B2;
      var_B2=arg_B1-arg_B2;
      return var_B1*var_B2;
}
intfunc_A(int arg_A1, int arg_A2)
{
      int var_A;
      var_A = func_B(arg_A1,arg_A2) + arg_A1 ;
      return var_A;
}
int main(int argc, char **argv, char **envp)
{
      int var_main;
      var_main=func_A(4,3);
      return var_main;
}

這段代碼經過編譯器編譯后,各個函數對應的機器指令在代碼區中可能是這樣分布的,如圖2.1.2所示。

根據操作系統的不同、編譯器和編譯選項的不同,同一文件不同函數的代碼在內存代碼區中的分布可能相鄰,也可能相離甚遠,可能先后有序,也可能無序,;但它們都在同一個PE文件的代碼所映射的一個“節”里。我們可以簡單地把它們在內存代碼區中的分布位置理解成是散亂無關的。

當CPU在執行調用func_A函數的時候,會從代碼區中main函數對應的機器指令的區域跳轉到func_A函數對應的機器指令區域,在那里取指并執行;當func_A函數執行完閉,需要返回的時候,又會跳回到main函數對應的指令區域,緊接著調用func_A后面的指令繼續執行main函數的代碼。在這個過程中,CPU的取指軌跡如圖2.1.3所示。

圖2.1.2 函數代碼在代碼區中的分布示意圖

圖2.1.3 CPU在代碼區中的取指軌跡示意圖

那么CPU是怎么知道要去func_A的代碼區取指,在執行完func_A后又是怎么知道跳回到main函數(而不是func_B的代碼區)的呢?這些跳轉地址我們在C語言中并沒有直接說明,CPU是從哪里獲得這些函數的調用及返回的信息的呢?

原來,這些代碼區中精確的跳轉都是在與系統棧巧妙地配合過程中完成的。當函數被調用時,系統棧會為這個函數開辟一個新的棧幀,并把它壓入棧中。這個棧幀中的內存空間被它所屬的函數獨占,正常情況下是不會和別的函數共享的。當函數返回時,系統棧會彈出該函數所對應的棧幀。

如圖2.1.4所示,在函數調用的過程中,伴隨的系統棧中的操作如下。

·在main函數調用func_A的時候,首先在自己的棧幀中壓入函數返回地址,然后為func_A創建新棧幀并壓入系統棧。

圖2.1.4 系統棧在函數調用時的變化

·在func_A調用func_B的時候,同樣先在自己的棧幀中壓入函數返回地址,然后為func_B創建新棧幀并壓入系統棧。

·在func_B返回時,func_B的棧幀被彈出系統棧,func_A棧幀中的返回地址被“露”在棧頂,此時處理器按照這個返回地址重新跳到func_A代碼區中執行。

·在func_A返回時,func_A的棧幀被彈出系統棧,main函數棧幀中的返回地址被“露”在棧頂,此時處理器按照這個返回地址跳到main函數代碼區中執行。

題外話:在實際運行中,main函數并不是第一個被調用的函數,程序被裝入內存前還有一些其他操作,圖2.1.4只是棧在函數調用過程中所起作用的示意圖

2.1.4 寄存器與函數棧幀

每一個函數獨占自己的棧幀空間。當前正在運行的函數的棧幀總是在棧頂。Win32系統提供兩個特殊的寄存器用于標識位于系統棧頂端的棧幀。

(1)ESP:棧指針寄存器(extended stack pointer),其內存放著一個指針,該指針永遠指向系統棧最上面一個棧幀的棧頂。

(2)EBP:基址指針寄存器(extended base pointer),其內存放著一個指針,該指針永遠指向系統棧最上面一個棧幀的底部。

提示:EBP指向當前位于系統棧最上邊一個棧幀的底部,而不是系統棧的底部。嚴格說來,“棧幀底部”和“棧底”是不同的概念,本書在敘述中將堅持使用“棧幀底部”這一提法以示區別;ESP所指的棧幀頂部和系統棧的頂部是同一個位置,所以后面敘述中并不嚴格區分“棧幀頂部”和“棧頂”的概念。請您注意這里的差異,不要產生概念混淆。

寄存器對棧幀的標識作用如圖2.1.5所示。

圖2.1.5 棧幀寄存器ESP與EBP的作用

函數棧幀:ESP和EBP之間的內存空間為當前棧幀,EBP標識了當前棧幀的底部,ESP標識了當前棧幀的頂部。

在函數棧幀中,一般包含以下幾類重要信息。

(1)局部變量:為函數局部變量開辟的內存空間。

(2)棧幀狀態值:保存前棧幀的頂部和底部(實際上只保存前棧幀的底部,前棧幀的頂部可以通過堆棧平衡計算得到),用于在本幀被彈出后恢復出上一個棧幀。

(3)函數返回地址:保存當前函數調用前的“斷點”信息,也就是函數調用前的指令位置,以便在函數返回時能夠恢復到函數被調用前的代碼區中繼續執行指令。

題外話:函數棧幀的大小并不固定,一般與其對應函數的局部變量多少有關。在后面調試實驗中您會發現,函數運行過程中,其棧幀大小也是在不停變化的。

除了與棧相關的寄存器外,您還需要記住另一個至關重要的寄存器。

EIP:指令寄存器(Extended Instruction Pointer),其內存放著一個指針,該指針永遠指向下一條等待執行的指令地址,其作用如圖2.1.6所示。

可以說如果控制了EIP寄存器的內容,就控制了進程——我們讓EIP指向哪里,CPU就會去執行哪里的指令。在本章第4節中我們會介紹控制EIP劫持進程的原理及實驗。

2.1.5 函數調用約定與相關指令

函數調用約定描述了函數傳遞參數方式和棧協同工作的技術細節。不同的操作系統、不同的語言、不同的編譯器在實現函數調用時的原理雖然基本相同,但具體的調用約定還是有差別的。這包括參數傳遞方式,參數入棧順序是從右向左還是從左向右,函數返回時恢復堆棧平衡的操作在子函數中進行還是在母函數中進行。表2-1-1列出了幾種調用方式之間的差異。

表2-1-1 調用方式之間的差異

具體的,對于Visual C++來說,可支持以下3種函數調用約定,如表2-1-2所示。

表2-1-2 函數調用約定

如果要明確使用某一種調用約定,只需要在函數前加上調用約定的聲明即可,否則默認情況下,VC會使用__stdcall的調用方式。本篇中所討論的技術在不加額外說明的情況下,都是指這種默認的__stdcall調用方式。

除了上邊的參數入棧方向和恢復棧平衡操作位置的不同之外,參數傳遞有時也會有所不同。例如,每一個C++類成員函數都有一個this指針,在Windows平臺中,這個指針一般是用ECX寄存器來傳遞的,但如果用GCC編譯器編譯,這個指針會作為最后一個參數壓入棧中。

提示:同一段代碼用不同的編譯選項、不同的編譯器編譯鏈接后,得到的可執行文件會有很多不同。因此,請您在進行后續實驗前務必注意實驗環境的描述,否則所得結果可能會與實驗指導有所差異。

函數調用大致包括以下幾個步驟。

(1)參數入棧:將參數從右向左依次壓入系統棧中。

(2)返回地址入棧:將當前代碼區調用指令的下一條指令地址壓入棧中,供函數返回時繼續執行。

(3)代碼區跳轉:處理器從當前代碼區跳轉到被調用函數的入口處。

(4)棧幀調整:具體包括。

保存當前棧幀狀態值,已備后面恢復本棧幀時使用(EBP入棧);

將當前棧幀切換到新棧幀(將ESP值裝入EBP,更新棧幀底部);

給新棧幀分配空間(把ESP減去所需空間的大小,抬高棧頂);

對于__stdcall調用約定,函數調用時用到的指令序列大致如下。

        ;調用前
push 參數3     ;假設該函數有3個參數,將從右向左依次入棧
push 參數2
push 參數1
call 函數地址;call指令將同時完成兩項工作:a)向棧中壓入當前指令在內存
                            ;中的位置,即保存返回地址。b)跳轉到所調用函數的入口地址函
                            ;數入口處
push ebp                    ;保存舊棧幀的底部
mov ebp,esp                ;設置新棧幀的底部(棧幀切換)
sub esp,xxx                ;設置新棧幀的頂部(抬高棧頂,為新棧幀開辟空間)

上面這段用于函數調用的指令在棧中引起的變化如圖2.1.7所示。

題外話:關于棧幀的劃分,不同參考書中有不同的約定。有的參考文獻中把返回地址和前棧幀EBP值做為一個棧幀的頂部元素,而有的則將其做為棧幀的底部進行劃分。在后面的調試中,您會發現OllyDbg在棧區標示出的棧幀是按照前棧幀EBP值進行分界的,也就是說,前棧幀EBP值既屬于上一個棧幀,也屬于下一個棧幀,這樣劃分棧幀后,返回地址就成為了棧幀頂部的數據。出于前后概念一致的目的,在本書中將堅持按照EBP與ESP之間的部分做為一個棧幀的原則進行劃分。這樣劃分出的棧幀如圖2.1.7最后一幅圖所示,棧幀的底部存放著前棧幀EBP,棧幀的頂部存放著返回地址。劃分棧幀只是為了更清晰地了解系統棧的運作過程,并不會影響它實際的工作。

類似地,函數返回的步驟如下。

(1)保存返回值:通常將函數的返回值保存在寄存器EAX中。

(2)彈出當前棧幀,恢復上一個棧幀。

具體包括:

·在堆棧平衡的基礎上,給ESP加上棧幀的大小,降低棧頂,回收當前棧幀的空間。

·將當前棧幀底部保存的前棧幀EBP值彈入EBP寄存器,恢復出上一個棧幀。

·將函數返回地址彈給EIP寄存器。

圖2.1.7 函數調用時系統棧的變化過程

(3)跳轉:按照函數返回地址跳回母函數中繼續執行。

還是以C語言和Win32平臺為例,函數返回時的相關的指令序列如下。

addesp, xxx ;降低棧頂,回收當前的棧幀
pop ebp;將上一個棧幀底部位置恢復到ebp,
retn;這條指令有兩個功能:a)彈出當前棧頂元素,即彈出棧幀中的返回地址。至此,
;棧幀恢復工作完成。b)讓處理器跳轉到彈出的返回地址,恢復調用前的代碼區

按照這樣的函數調用約定組織起來的系統棧結構如圖2.1.8所示。

題外話:Win32平臺下有很多寄存器,Intel指令集中的指令也有很多,現在立刻逐一介紹它們無疑相當于給已經滿頭霧水的您再澆一桶冷水。雖然這里僅僅列出了3個寄存器和幾條指令的作用,但只要您完全理解它們,就一定能順利理解本書的后續章節,因為它們是棧溢出利用的關鍵,也是計算機架構的核心所在。當然,入門以后要想提高到一個新的層次,用《IBM X86匯編》或者《Win32匯編》惡補一下匯編知識是非常必要的。

圖2.1.8 函數調用的實現

主站蜘蛛池模板: 阳信县| 衢州市| 临桂县| 江西省| 大关县| 康保县| 安平县| 东辽县| 鄂尔多斯市| 谷城县| 北海市| 都匀市| 玉溪市| 永寿县| 甘泉县| 鹤壁市| 抚州市| 舒兰市| 金昌市| 长沙市| 昔阳县| 穆棱市| 浮山县| 旺苍县| 海宁市| 清河县| 东兴市| 高雄市| 若尔盖县| 胶南市| 宽甸| 湘阴县| 化隆| 昆山市| 安顺市| 湟中县| 弥渡县| 灵丘县| 潍坊市| 泌阳县| 榆林市|