CSAPP buffer lab記錄——IA32版本

來源:互聯網
上載者:User

標籤:目標   課程   系統   基礎   return   add   分析   利用   raw   

  CSAPP buffer lab為深入理解電腦系統(原書第二版)的配套的緩衝區溢位實驗,該實驗要求利用緩衝區溢位的原理解決5個難度遞增的問題,分別為smoke(level 0)、fizz(level 1)、bang(level 2)、boom(level 3)、kaboom(level 4).在實踐中加深對函數調用和緩衝區溢位機制的理解(針對IA-32體繫結構)。

   本記錄使用的是取自原書配套網站的self-study handout版本,網址為http://csapp.cs.cmu.edu/2e/labs.html。

   原課程實驗指導為CSAPP buffer lab writeup:http://csapp.cs.cmu.edu/2e/buflab.pdf

   關於實驗中gdb、gcc的使用問題可以參考筆者另一篇部落格:Linux下編輯、編譯、調試命令總結——gcc和gdb描述

   Buffer lab的self-study handout版本為一個 .tar 打包檔案。解壓可參考文章Linux下檔案的打包、解壓縮指令——tar,gzip,bzip2

 

實驗準備:

  解壓得到包含三個可執行檔的名為buflab-handout的目錄:

  bufbomb:用來攻擊的緩衝區炸彈程式;

  hex2raw:用來進行十六進位向二進位串轉換的工具(可能所需輸入並不是ASCII的可列印字元,藉助此工具進行轉換);

  makecookie:在實驗中是根據ID產生特定的cookie的工具,在self-study中不使用;

 

實驗分析:(更多請參見buffer lab的writeup介紹)

getbuf()

  bufbomb通過一個自訂的getbuf()函數讀取輸入,並將其複製到目標記憶體中。該函數主要通過Gets()函數實現。 

/* Buffer size for getbuf */#define NORMAL_BUFFER_SIZE 32 int getbuf() {    char buf[NORMAL_BUFFER_SIZE];    Gets(buf);    return 1; }

  Gets()函數從標準輸入中讀取輸入(以"\n"或者EOF作為輸入結尾,並不檢查讀取長度),並將其以"\n"結尾存放在指定的記憶體地區中。這裡可知目標字元數組buf長度為32位元組。

  注意:buf數組的長度為32個位元組,但是數組實際在記憶體(棧)中被分配的長度和位置隨編譯器的不同而有所區別,這裡由於直接給出了可執行程式,可以使用objdump -d bufbomb 查看getbuf的實現。

  

  根據函數調用時參數入棧的規則,可知調用Gets函數之前buf數組的首地址會入棧,易知%eax儲存的 %ebp - 40 為buf數組的首地址,這裡可以看出已指派的空間和數組長度並不嚴格相同!(為滿足資料對齊,IA32保證每個棧幀的長度為16的整數倍)。getbuf的棧結構如下:

  

 

  當輸入不足32個位元組時,輸出如下:

  

  輸入超過32個位元組時,輸出如下:

  

  

  一般使用命令列./bufbomb -u IDname運行bufbomb,程式會根據輸入的IDname產生特定的cookie(見)。實驗過程中需根據實驗要求,構造能夠實現特定功能的輸入序列(在電腦中是二進位的形式),完成對應的五個任務。

  hex2raw:由於輸入序列所需要的二進位串可能無法完全對應ASCII中的可列印字元,如地址高位為0x80時,ASCII對應的字元無法通過鍵盤輸入(可參考xxxxx),可使用hex2raw進行轉換。hex2raw讀取輸入檔案中的十六進位數串,並將兩個十六進位數轉換為對應位元組的二進位流,如想要輸入高位地址0x80時,直接在輸入檔案中寫入其對應的十六進位表示 80,hex2raw自動將其轉換為對應的二進位序列。可使用 cat filename| ./hex2raw| bufbomb 輸入構造的字串流,其中filename為存放有帶轉換的十六進位串的檔案。(這裡需要注意,實驗所提供的hex2rax工具為64位版本,可通過命令 file hex2raw 查看。若在32位環境下運行,會提示cann‘t execute binary file:format error.同時,bufbomb檔案位32位版本,)

  實驗的目的便是構造正確的字串輸入,實現特定的功能,從而完成五個任務。

 

實驗過程:

level 0 Candle(10 pts)

  在bufbomb中存在函數test(),其調用getbuf()函數讀取輸入,並通過uniqueval()函數進行堆棧是否被破壞的檢查,之後根據讀取後的情況進行相應的輸出。

void test()2 {3 int val;4 /* Put canary on stack to detect possible corruption */5 volatile int local = uniqueval();67 val = getbuf();89 /* Check for corrupted stack */10 if (local != uniqueval()) {11 printf("Sabotaged!: the stack has been corrupted\n");12 }13 else if (val == cookie) {14 printf("Boom!: getbuf returned 0x%x\n", val);15 validate(3);16 } else {517 printf("Dud: getbuf returned 0x%x\n", val);18 }19 }

  同時,bufbomb檔案中還存在函數smoke(),level 0即改變程式控制流程,使得test函數調用getbuf()後,在getbuf()返回時直接調用smoke()函數,而不是返回函數test()

void smoke(){printf("Smoke!: You called smoke()\n");validate(0);exit(0);}

  這裡主要考察函數調用時返回地址相關的知識。我們知道在函數調用過程中,控制權跳轉至目標函數之前,會將返回地址(調用點處的下一條指令的地址)入棧,在函數調用結束後,通過該地址來繼續執行調用點的下一條指令。這裡,想要在函數調用結束時直接調用smoke()函數,主要在於修改函數調用的返回地址為smoke()函數的地址。注意,雖然test中有檢查堆棧破環的canary,但任務目的是在getbuf結束後直接調轉至另一函數,而沒有執行後續的堆棧是否被破壞的檢查,所以可直接構造超出數組長度的字串來覆蓋返回地址,使其指向目標函數的地址。

  由前文對getbuf的棧空間的分析,可知想要構造覆蓋返回地址的函數,則字串的結構應為  44個位元組的填充字元(buf分配的空間+ebp) + 新的返回地址。

  通過gdb動態調試可知smoke函數起始地址為0x08048c18.

  

  構造的字串為:0*44 + 18 8c 04 08 .(小端法存放)。構造的用於hex2raw轉換的檔案,其中30為字元0對應的ASCII的十六進位表示,最後4個位元組位小端法表示的smoke函數起始地址。

   

  成功調用了smoke函數:

  

 

 

level 1 Sparkler(10 pts)

  bufbomb中存在另一個函數fizz(),其源碼如下.fizz()函數將實參與cookie比較,當cookie與實參相等時,則表示成功。這裡的cookie即為運行./bufbomb -u UesrID 時根據UserID產生的cookie。

void fizz(int val){if (val == cookie) {printf("Fizz!: You called fizz(0x%x)\n", val);validate(1);} elseprintf("Misfire: You called fizz(0x%x)\n", val);exit(0);}

  level 1的任務為(1)修改getbuf函數返回地址為fizz()函數,而不是返回函數test() ; (2)在fizz()中驗證成功,即需傳入cookie值作為參數;

  這裡主要考察的是關於函數的參數傳遞方面的知識。

  下左圖:

  (1)對於有參數的被調函數,函數調用之前會將參數按從右至左的順序入棧,之後在被調函數中通過%ebp+8、%ebp+12等地址獲得函數調用的實參。

  (2)函數調用指令call會將函數的返回地址入棧,被調函數會將原函數的棧幀指標即儲存在%ebp中的值入棧,並使新的%ebp的值等於%esp,從而使%ebp指向被調函數的棧幀,這樣新%ebp指向的地址與函數的參數存放處間隔儲存的%ebp和返回地址,故可以通過%ebp+8獲得函數的第一參數

             

  上右圖:

  (1)左側箭頭標識%esp位置。函數返回時,首先將%ebp值賦值給%esp,則棧頂為位置(1).之後push %ebp,將%ebp還原,%esp在位置(2)。最後ret指令恢複返回地址,%esp指向位置(3);

  (2)由於需返回fizz函數,故返回地址已被修改為fizz的地址。注意這裡沒有call指令,沒有返回地址入棧。fizz函數按正常流程執行,其棧自紅線處開始。先將%ebp入棧,新的%ebp位置。fizz函數正常按照%ebp+8的位置取其參數,故圖示棧中的位置(1)應被覆蓋為cookie值;

 

  故構造的輸入字串應為:44個填充位元組 + fizz函數起始地址 + 4個填充位元組 + cookie值.fizz函數的起始地址可使用gdb查看,cookie值為bufbomb產生的值,這裡注意使用小端法書寫即可。

  

  

  構造的字串為:0*44 + 42 8c 04 08(fizz起始地址) + 0*4 +da 31 3e 37(cookie) .

   

  結果:

  

 

 

level 2 Firecracker(15 pts)

  更複雜的構造輸入字串的方法是在字串中包含有實際功能的機器語言代碼,並修改函數的返回地址使其指向構造的代碼,從而執行這段代碼實現設定的功能。

  bufbomb中存在函數bang,函數同樣驗證cookie與實參的值,並根據結果進行驗證,同時輸出全域變數global_value的值。

int global_value = 0;void bang(int val){if (global_value == cookie) {printf("Bang!: You set global_value to 0x%x\n", global_value);validate(2);} elseprintf("Misfire: global_value = 0x%x\n", global_value);exit(0);}

  level 2的任務為:(1)通過執行輸入構造的機器指令修改global_value的值為cookie的值; (2)如level 0中所執行的,test函數調用getbuf()後,在getbuf()返回時直接調用fizz()函數,而不是返回函數test() ;

  任務的關鍵在於如何構造機器代碼,以及使得程式跳轉至輸入的機器代碼處執行,同時注意函數bang的參數傳遞過程

  構造輸入字串的過程:

  (1)全域變數global_value在程式執行的過程中邏輯地址不發生變化,可直接在gdb中得到其地址,使用mov指令對其進行賦值;

  (2)將getbuf函數的返回地址修改,指向構造的機器代碼的開始處,這裡即buf數組的起始地址;

  (3)由於getbuf函數的返回地址已經被用於指向輸入的機器代碼,故跳轉至bang函數的實現需要使用額外的指令。這裡由於程式是已經編譯好的,所以bang函數的邏輯地址不變,故可以直接使用邏輯地址調用。使用push 將bang函數地址入棧,再使用ret指令進行跳轉。(push指令將資料放置在棧頂,ret取棧頂的資料並將其作為地址進行跳轉);

  (4)這裡需要注意函數bang的參數傳遞過程。之前的level 0與level 1,機器代碼存放在程式碼片段,由PC指示,資料操作在棧上,由%esp指示。level 2中第一次跳轉後,正在執行的機器代碼位於棧上的緩衝區中,由PC指示,資料操作也在棧上,由%esp指示,這裡需要注意兩者的區別,前者是用於執行的,後者用於操作。圖示為函數ret指令之後%esp和PC的位置。同樣,對函數參數的傳遞可參考level 1或者直接使用%esp + 4定址賦值;

  

  構造輸入字串:可執行檔機器代碼 + 填充字元 + 指向輸入機器代碼的地址。

  通過gdb得到bang函數的起始地址位0x08048c9d。

  

  同樣在bang函數的反組譯碼中,將0x804d100與0x804d108處的值進行了比較,查看地址0x804d108,發現存放的是cookie,則0x804d100處即為全域變數global_value的值。

  

  在getbuf函數內部設定斷點,並運行至函數內部,得到buf數組的起始地址為0x55683978.(這裡注意要運行至getbuf內部是由於需要使得%ebp指向的是getbuf的棧幀,這樣%ebp-40才是buf數組的首地址,否則直接輸出%ebp-40可能指向的其他地方)

  

  構造的可執行代碼為:

mov $0x373e31da,0x804d100            #將cookie值賦值給global_valuemov 0x804d100,%eaxmov %eax,4(%esp)                     #將global_value的值作為實參放置在棧中作為bang的參數push $0x08048c9d                     #將bang函數起始地址入棧,注意常數的書寫方式,加上$ret                                  #將棧頂資料作為地址進行跳轉

  可以將上訴彙編指令進行編譯,編譯方法可見實驗writeup的最後一部分,再使用objdump或gdb反組譯碼來得到所需的機器代碼的十六進位表示。(注意這裡和gdb中的代碼並沒有明確給出指令尾碼b、w、l、q,但在實際書寫中需加上尾碼才能編譯)

 

  實際使用的輸入字串,其中可執行代碼(25bytes) + 填充字元(19bytes) + 數組首地址(4bytes)

  

  執行後的結果為:

  

 

 

level 3 Dynamite(20 pts)

  目前為止的操作都是使得正常控制流程改變並跳轉至其它函數,最終使得程式停止,故而以上操作中對於棧的破壞、破壞保留值等操作對於程式運行是可以接受的。更複雜的緩衝區攻擊在於執行某些構造的指令改變寄存器或記憶體中的值,並使程式能正常返回原控制流程執行。level 3的任務為通過構造指令,使得getbuf正常返回至test函數,並使得getbuf傳回值為cookie值

  需要注意以下幾點:

  (1)構造的機器指令是存放在getbuf的緩衝區中,想要執行輸入的構造代碼,只有修改geubuf函數返回時的地址,注意當跳轉至構造的代碼處執行時,getbuf是已經結束了,傳回值1存放在寄存器%eax中;(正是結束時的ret指令才跳轉至修改後的地址處)

  (2)回想函數調用過程,call指令調用函數時將返回地址放置在棧頂,進入函數後的第一步為儲存%ebp,這樣在覆蓋修改返回地址時必將覆蓋儲存的%ebp也覆蓋掉了。在getbuf函數結束時,會將%esp的值賦值為getbuf棧幀指標%ebp的值(mov指令),之後將儲存的%ebp值賦值給寄存器%ebp。前面所述,儲存的%ebp在覆蓋返回地址時已經被覆蓋,故此時%ebp會是一個任意值;

  (3)由於題目的要求是正常返回test函數,而該函數存在一定的對緩衝區覆蓋的檢查(uniqueval函數),故可能需要注意構造的字串的長度;

  如上所述,構造的字串應完成的功能為:(1)修改存放傳回值的寄存%eax; (2)恢複寄存器%ebp的值為正常值,這裡即test函數的棧幀; (3)將getbuf正常返回地址放置在棧頂,並通過ret指令返回test函數。

  通過gdb調試,在getbuf函數內部設定斷點,查看儲存的返回地址、儲存的%ebp等資訊。p $ebp獲得getbuf棧幀指標的資訊,再使用x /2xw $ebp獲得地址%ebp處開始的連續兩個4位元組空間的值(回憶一下getbuf的棧結構,這兩個空間存放的即為儲存的%ebp和返回地址)。得到returnaddress為0x08048dbe,儲存的%ebp為0x556839d0.

  

  構造的可執行代碼為: 

movl $0x373e31da,%eax    #修改傳回值為cookie值movl $0x556839d0,%ebp    #恢複被破壞的儲存的%ebp的值push $0x08048dbe       #將返回地址入棧ret               #跳轉

  構造的字串序序列如下,其中 構造代碼(16位元組) + 填充字元(28位元組) + 修改的返回地址,即為數組起始地址(4位元組)

  

  執行結果如下:

  

 

 

level 4 Nitroglycerin(10 pts)

  對於一個給定的程式而言,程式每次運行時尤其是被不同使用者運行時的使用的棧位置是不同的。造成棧位置變化的原因有很多,其中一個是由於程式在運行時,所有必要的環境變數都以字串的形式被放置在棧的底部(高地址單元)。對於不同值的環境變數,其所需要的棧空間自然不同,從而使得棧位置的變化,對於不同使用者而言這一點更為顯著。相應的,程式自然運行與在gdb環境下啟動並執行棧位置也可能不同,因為部分gdb本身運行所需的資料被放置在了棧中。

  對於getbuf函數而言,其內建了使棧空間穩定的特性,從而使得進行緩衝區攻擊時能夠直接獲得所需要的地址資料,並採用直接利用的方式寫入機器代碼中,這也大大降低了實現難度。而這在實際應用情況下是過分理想的。對於level 4,需要在啟動bufbomb時使用 -n 選項,從而使得棧空間不再穩定,並在此基礎上進行基於緩衝區溢位原理的實驗。

  程式運行時啟用了 -n 選項時,程式在讀取輸入時會啟用 getbufn函數(而不是前面的getbuf)。getbufn函數有與getbuf相似的功能,但前者輸入數組的長度為512位元組。調用getbufn函數之前,程式會先在棧上分配一個隨機長度的空間,從而使得getbufn函數的棧空間在不同調用情況下不再是固定的,實際上%ebp的差值達到±240。同時,在應用 -n 選項的情況下,程式會要求提交輸入字串 5 次,5次輸入會面對5個不同的棧空間,並要求每次都成功返回cookie值。level 4的任務與level 3一致,即要求getbufn函數返回調用函數testn時返回cookie值,而不是常規的1.

  

  這裡程式的運行過程加入了棧隨機化的操作,即在程式調用之前,首先分配一個隨機大小的空間,這個空間程式並不使用,但是長度不定,從而使得每次運行時的棧空間產生變化(主要是在棧相對結構不變的情況下,各個棧中元素的地址發生了變化),見圖一。這一操作的顯著影響是之前所採用的使用固定的新返回地址覆蓋getbufn返回地址的方法受到限制。由於每次棧空間不同,則輸入的機器代碼的起始位置也不同(回憶上文,每次均是將機器代碼放在輸入字串的開始位置,這樣每次修改返回地址為輸入數組的起始地址即可執行構造的代碼,其中輸入數組起始地址是固定的),則難以直接指定出構造代碼的地址。

  這裡對於棧隨機化的破解主要藉助於“空操作雪橇”(nop sled)的操作。所謂nop sled是在構造的機器代碼之前加入nop指令(no operation的縮寫,機器碼位 0x90),其作用為僅將PC增加而不執行任何操作。在這種情況下,只要覆蓋的地址能夠指向nop序列所處的任意一個地址,就可以順序執行nop指令,直到遇到真正構造的機器代碼,這樣的情況下,對於用於覆蓋的返回地址的要求就降低了。

即構造出的字串位:nop指令串 + 構造的機器代碼 + 返回地址。在本實驗的操作過程中,我們主要是通過確定一個固定的返回地址來覆蓋。

  

  查看getbufn函數的實現,可知數組的分配的長度位520個位元組(0x208),覆蓋返回地址需要填充 520(數組長度)+ 4(儲存的%ebp)  = 524個位元組。

  

  通過 p $eax查看%ebp的值,通過 x /2xw $ebp 查看儲存的%ebp和返回地址的值。

  

   

  解題思路如下:

  (1)為達到能返回cookie值至testn函數的目的,同樣需要getbufn修改返回地址使其執行構造的代碼,完成包括修改傳回值、恢複%ebp、返回testn函數這三個步驟;

  (2)在步驟(1)中,修改傳回值即%eax與返回testn函數的操作與level 3是一樣的。總是將傳回值修改為cookie,返回testn函數的地址也總是不變的(注意這裡程式應用的是棧隨機化的操作,影響的是棧空間上的地址,可執行代碼是存放在程式碼片段,在題設環境下是不受影響的);

  (3)關於如何恢複被覆蓋%ebp的問題。棧隨機化是在棧上分配一段不定長的記憶體空間使得棧中元素的地址發生變化。但是,由於程式總是執行相同的操作,使得在不同的執行情況下,程式所使用的棧中元素的相對位置(距離)不發生變化,可嘗試在此前提下恢複%ebp。恢複過程是由輸入的構造代碼執行的,此時%ebp已經被賦予了“廢值”(見level 3分析),但%esp是有效值,可以通過%esp推出被覆蓋的儲存的%ebp的值。從上面獲得的儲存的%ebp的值和%ebp的值,可以看到在差值為0x38.xxxxxxxxxxxxxxxxxxxxxxx

  //圖圖圖

構造的字串序列為: nop指令串() + 可執行程式碼片段() + 用於覆蓋的地址(4位元組)

  

確認了輸入進行調試時,可分別在0x0804921b處和call指令之後設定斷點,分別對應讀取輸入前後的狀態,用於驗證構造的字串是否產生預期效果。

  以下是藉助gdb偵錯工具的過程中兩次運行時的棧空間的變化。可以看到,在兩次運行中,%ebp和儲存的%ebp改變了,而返回地址沒有改變,這是由於返回地址指向的是位於程式碼片段的固定位置處的代碼,不受棧隨機化的影響,但位於棧上的資料則受到了影響。

  

  

  構造的可執行代碼為:

movl $373e31da,%eax    //修改傳回值movl %esp,%ebx      addl $0x28,%ebx  movl %ebx,%ebp       //根據%esp的值得到需要恢複的%ebp的值pushl $0x08048e3a      ret             //跳轉至原函數

  

  構造的輸入字串為 :nop指令串(506位元組) + 構造指令(18位元組) + 用於覆蓋的新地址(4位元組)

  

  最終結果為:

  

CSAPP buffer lab記錄——IA32版本

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.