◆李維峰
(中國飛行試驗研究院 陜西 710089)
棧緩存溢出漏洞自1988年出現至今已過去30多年了[1],它以其危害性及廣泛性早已引起了廣大信息安全領域研究人員的重視。多年以來,隨著攻防雙方技術的交替進步,對于此類漏洞的控制已經不像最初階段那么無力,但是不得不說,由于棧緩存溢出漏洞的產生根源是程序設計不嚴謹所導致,換個更直接的說法就是該漏洞的產生可認為是人為的編碼疏忽,因此,在未來很長的一段時間內它還將與我們共存。本文將利用Metasploit框架開發一個針對軟件bof-server棧緩存溢出漏洞的破解模塊,以此為例,講解此類漏洞的危害及預防措施。由于Metasploit框架是進行滲透測試的優秀工具,它提供了大量的漏洞滲透模塊庫,集成了優秀的模塊開發環境,所以,我們選擇它作為本文的實驗工具。
在微軟的定義里,緩存溢出攻擊是一種攻擊者用自己的代碼覆蓋程序原有代碼的行為,如果被覆蓋的惡意代碼是一段受攻擊者控制的可執行代碼,那么攻擊者就可以在目標系統中進行意料之外的操作。
棧是一種數據結構,棧中數據的寫入和讀取只能從棧頂進行操作,它遵循后進先出的原則。棧支持兩種操作:push和pop。push是將數據添加到棧頂。pop是將數據從棧頂彈出。讓我們看一下C程序的內存布局、它的內容以及它在函數調用和返回期間是如何工作的。如圖1所示。
其中,Text:包含要執行的程序代碼。Data:包含程序的全局信息。Stack:包含函數參數,返回地址和函數的局部變量。它是后進先出的數據結構。隨著新函數的調用,它在內存中向下增長(從較高的地址空間到較低的地址空間)。Heap:容納所有動態分配的內存。每當我們使用malloc動態獲取內存時,它都是從堆中分配的。隨著需要越來越多的內存,堆在內存中的增長(從低到高)。
對于基于棧的緩存溢出,我們把注意力集中在寄存器EBP、EⅠP和ESP上。EBP指向堆棧底部的較高內存地址,ESP則指向堆棧頂部的較低內存位置。EⅠP中存儲的是下一條指令的地址。我們主要關注EⅠP寄存器,因為我們需要劫持程序的執行順序。由于EⅠP只是一個寄存器,所以我們無法為其分配要執行指令的內存地址。

圖1 C程序中的內存結構

圖2 棧的內存結構
當函數執行時,一個包含有函數信息的棧幀(stack frame)會被壓入棧中。一旦函數執行完畢,棧幀會被彈出棧,函數完成執行后,將從棧中彈出相關的棧幀,并在中斷的調用的函數中繼續執行。CPU知道必須從何處繼續執行程序,它是從調用函數時壓入棧的返回地址獲得此信息。
為了方便理解,我們舉一個例子:在主函數中調用func()。因此,當程序開始時,將調用main(),并為其分配一個棧幀并將其壓入棧。接下來main()調用func(),同樣是分配棧幀,將其壓入棧并將執行移交給func(),main()通過將這個值(返回地址)壓入棧,來指出當func()返回(通常是在調用func()之后的代碼行)時,需要繼續執行的地方。

圖3 棧幀結構
在func()函數執行完后,它的棧幀被彈出,其中存儲的返回地址被加載到EⅠP寄存器中,繼續執行main()。如果我們能夠控制返回地址,我們就能在func()返回時劫持將要執行的指令。
首先,我們下載并運行bof-server。可以看到這個程序在端口200上提供TCP服務。如圖4所示:

圖4 程序在端口200上提供TCP服務
然后,我們向TCP 200端口發起TELNET連接,建立連接后向其發送若干隨機數據。如圖5所示:

圖5 建立連接后向其發送若干隨機數據
我們發現,當提供一定數量的隨機數據之后,連接就斷開了,這是因為目標服務器已經崩潰。來看一看目標服務器上的報錯信息,如圖6所示:

圖6 目標服務器上的報錯信息
點擊“click here”,查看詳細情況,發現程序是由于無法在地址41414141處找到下一條要執行的指令,從而導致了程序的崩潰。因為我們隨機輸入的是若干個字母A,而值41就是字母A的十六進制表示,這說明我們輸入的數據已經超出了緩存的范圍,而且覆蓋了EⅠP寄存器。接下來,程序試圖執行41414141這個地址上指令,顯然這不是一個有效的地址,因此,程序崩潰了。
由于我們的輸入數據超出了程序棧的緩存范圍,引發了程序的棧緩存溢出漏洞,導致程序崩潰。如果我們控制好輸入的數據,使得覆蓋EⅠP寄存器的內容恰好是我們想要執行的代碼地址,那么我們就控制了服務器,從而完成了漏洞利用。
根據上一節的思路,我們將利用Metasploit框架開發一個破解模塊,觸發漏洞并運行我們想要執行的其他代碼。
模塊開發的第一個步驟是找出偏移量,在這個過程中將用到Metasploit中的兩款工具,分別是pattern_create和pattern_offset。工具pattern_create用來按一定規律生成字符,例如:#./pattern_create.rb 1000,表示生成1000個字符。將這些字符發送給目標服務器后如果程序崩潰,就能得到一個地址的值,我們得到的地址值是72413372,將該值作為參數,使用工具pattern_offset就能得到具體的偏移量,例如:#./pattern_offset.rb 72413372 1000,表示EⅠP中的地址為72413372,填充1000個字符。最后,我們得出的偏移量是520,在520個字節后面的4個字節的數據就會覆蓋EⅠP寄存器。
接下來,我們還將使用Metasploit中的另一個工具msfpescan來找到程序中JMP ESP指令的地址,在這里我們利用bof-server程序調用的一個DLL 文件ws2_32.dll 。命令如下:#./msfpescan -j esp -f/root/Desktop/ws2_32.dll,參數-j后面的是寄存器名,這里用到的寄存器是ESP,返回結果為0x71ab9372,這是ws2_32.dll文件中JMP ESP指令的地址。只需要用這個地址來重寫EⅠP寄存器中的內容,就可以執行我們的代碼。
到目前為止,我們已經掌握了開發Metasploit程序破解模塊的主要內容,讓我們看看代碼是怎樣的。如下所示:


在分析代碼之前,我們先看看模塊中用到的庫,請看表1。

表1 模塊中用到的庫
破解模塊開頭就是包含各種必要的路徑和文件。我們把模塊類型定義為Msf::Exploit::Remote,意味著它是一個遠程破解模塊。接下來,我們在initialize方法中定義name,description,author等基本信息。另外,我們還看到大量的其他聲明。請看表2:

表2 破解模塊

避免程序崩潰或payload 不執行
讓我們看看上面代碼中用到的一些重要函數,如表3。

表3 重要函數
在我們之前編寫的模塊中,run方法是輔助模塊的默認方法。然而,對于破解模塊而言,默認的方法是exploit。
我們使用connect連接目標。使用make_nops函數創造520個NOP,這個數來自initialize函數中定義的target的Offset。把520個NOPs存儲到變量buf中。下一條指令,我們通過從target聲明的Ret字段中獲取其值,將JMP ESP地址附加到buf。使用函數pack(‘V’),我們得到地址的小端格式。在Ret地址之后,我們附加幾個NOPs作為ShellCode之前的填充。使用Metasploit的優點之一是能在運行中切換payload。因此,只需要簡單地使用payload.encoded就能把當前所選的payload附加在變量buf之后。

圖7 步驟1
接下來,使用函數sock.put建立與目標的連接,參數是buf。使用handler方法檢查目標是否被成功破解,如果成功破解則會建立連接。最后,用disconnect斷開連接。我們來看看使用的效果:我們設置必要的參數,payload設置為Windows/meterpreter/bind_tcp,這意味著到目標的直接連接。讓我們看看使用exploit命令后會發生什么(圖8)。

圖8 步驟2
顯然,我們編寫的破解模塊成功了,獲得一個meterpreter會話,通過該會話可以在目標服務器上進行非授權操作。
通過上述例子,我們發現棧緩存溢出漏洞雖然早在二十世紀八十年代就存在,但至今仍對信息系統安全有著重要的影響。為了規避該漏洞,目前一般有幾種做法:使內存執行的堆棧部分為非可執行文件;使用更加健壯的C和C++庫;通過編譯器保護返回地址;使用防火墻[2]等等。最理想的辦法是雇傭最好的程序員謹慎編碼,當然這本身就不是一件容易的事。因此,我們需要對程序進行嚴格的模糊測試,降低棧緩存溢出漏洞發生的概率。