修複緩衝區溢位問題

來源:互聯網
上載者:User

當 David LeBlanc 和我確定《Writing Secure Code》一書的目錄時,我們明確地意識到必須著重介紹緩衝區溢位問題,因為已經有太多的開發人員在編寫代碼時犯了太多的此類錯誤,這些錯誤導致了可被人利用的緩衝區溢位的出現。在本文中,我將集中介紹為什麼會出現緩衝區溢位及其修複的方法。為什麼會出現緩衝區溢位

出現緩衝區溢位需要具備很多條件,包括:

  • 使用非型別安全的語言,如 C/C++。
  • 以不安全的方式訪問或複製緩衝區。
  • 編譯器將緩衝區放在記憶體中關鍵資料結構旁邊或鄰近的位置。

現在我們來仔細看看以上每種條件。

  首先,緩衝區溢位主要出現在 C 和 C++ 中,因為這些語言不執行數組邊界檢查和型別安全檢查。C/C++ 允許開發人員建立非常接近硬體啟動並執行程式,從而允許直接存取記憶體和電腦寄存器。其結果可以獲得優異的效能;很難有任何應用程式能象編寫得很好的 C/C++ 應用程式運行得那樣快。其他語言中也會出現緩衝區溢位,但很少見。如果出現這種錯誤,通常不是由開發人員造成的,而是運行時環境的錯誤。
  其次,如果應用程式從使用者(或攻擊者)那裡擷取資料,並將資料複製到應用程式所維護的緩衝區中而未考慮目標緩衝區的大小,則可能造成緩衝區溢位。換句話說,代碼為緩衝區分配了 N 個位元組,卻將多於 N 個位元組的資料複製到該緩衝區中。這就象向 12 盎司的玻璃杯中注入 16 盎司的水一樣。那麼多出的 4 盎司水到哪裡去了呢?全溢出去了!
  最後一點,也是最重要的一點,編譯器通常將緩衝區放在“令人感興趣的”資料結構旁邊。例如,當某個函數的緩衝區緊鄰堆棧,則在記憶體中該函數的返回地址緊靠在緩衝區之後。這時,如果攻擊者可以使該緩衝區發生溢出,他就可以覆蓋函數的返回地址,從而在返回函數時,返回到攻擊者定義的地址。其他令人感興趣的資料結構包括 C++ V 表、例外處理常式地址、函數指標等等。

下面我們來看一個樣本。

以下代碼有什麼錯誤?

void CopyData(char *szData) {   char cDest[32];   strcpy(cDest,szData);   // 使用 cDest   ...} 

令人驚訝的是,這段代碼可能沒有什麼錯誤!這完全取決於 CopyData() 的調用方式。例如,以下代碼是安全的:

char *szNames[] = {"Michael","Cheryl","Blake"};CopyData(szName[1]);

  這段代碼是安全的,因為名字是硬式編碼,並且知道每個字串在長度上不超過 32 個字元,因此調用 strcpy 永遠是安全的。然而,如果 CopyData 和 szData 的唯一參數來自不可靠的源(如通訊端或檔案),則 strcpy 將複製該資料,直到碰到Null 字元為止;如果此資料的長度大於 32 個字元,則 cDest 緩衝區將溢出,並且在記憶體中該緩衝區以外的任何資料將遭到破壞。不幸的是,在這裡,遭到破壞的資料是來自 CopyData 的返回地址,這意味著當 CopyData 完成時,它仍然在由攻擊者指定的位置繼續執行。這真糟糕!

其他資料結構也同樣敏感。假設某個 C++ 類的 V 表遭到破壞,如下面這段代碼:

void CopyData(char *szData) {   char cDest[32];   CFoo foo;   strcpy(cDest,szData);   foo.Init();}

  此樣本假定 CFoo 類具有虛方法,以及一個 V 表或該類方法的地址清單(與所有 C++ 類一樣)。如果由於 cDest 緩衝區被覆蓋而破壞了 V 表,則該類的任何虛方法(在此例中是 Init() )都可能調用攻擊者指定的地址,而不是 Init() 的地址。順便說一句,如果認為您的代碼不調用任何 C++ 方法就安全了,那就錯了,因為有一個方法始終會被調用,即該類的虛解構函式!當然,如果某個類不調用任何方法,就應該想想它存在的必要了。

修複緩衝區溢位

現在,我們繼續討論一些更實際的內容 - 如何在您的代碼中刪除和防止緩衝區溢位。 

遷移到Managed 程式碼

  在 2002 年 2 月和 3 月,我們舉辦了 Microsoft Windows Security Push 活動。在此期間,我的工作群組對 8,500 多位人員在設計、編寫、測試和記錄安全功能方面進行了培訓。我們為所有設計人員提出的一個建議就是,制定計劃,將相應的應用程式和工具從本機 Win32 C++ 代碼遷移到Managed 程式碼。這樣做有多種原因,主要是有助於減少緩衝區溢位。在Managed 程式碼中,很難建立出包含緩衝區溢位的代碼,因為所編寫的代碼不能直接存取指標、電腦寄存器或記憶體。您應當考慮,或者至少要計劃將某些應用程式和工具遷移到Managed 程式碼中。例如,管理工具就是一個很好的遷移對象。當然,我們也要現實一些,因為不可能在一個晚上將所有的應用程式從 C++ 遷移到 C# 或其他託管語言中。

遵循以下重要規則

當編寫 C 和 C++ 代碼時,應注意如何管理來自使用者的資料。如果某個函數具有來自不可靠源的緩衝區,請遵循以下規則:

  • 要求代碼傳遞緩衝區的長度。
  • 探測記憶體。
  • 採取防範措施。

現在我們來仔細看看以上每種情況。

要求代碼傳遞緩衝區的長度

如果任何函數調用具有類似特徵,將出現一個錯誤:

void Function(char *szName) {   char szBuff[MAX_NAME];   // 複製並使用 szName   strcpy(szBuff,szName);}

此代碼的問題在於函數不能判斷 szName 的長度,這意味著將不能安全地複製資料。函數應知道 szName 的大小:

void Function(char *szName, DWORD cbName) {   char szBuff[MAX_NAME];   // 複製並使用 szName   if (cbName < MAX_NAME)      strncpy(szBuff,szName,MAX_NAME-1);}

然而,您不能想當然地信任 cbName 。攻擊者可以設定該名稱和緩衝區大小,因此必須進行檢查!

探測記憶體

  如何判別 szName 和 cbName 是有效?您相信使用者會提供有效值嗎?一般來說,答案是否定的。驗證緩衝區大小是否有效一個簡單方法是探測記憶體。以下程式碼片段顯示了如何在代碼的調試版中完成這一驗證過程:

void Function(char *szName, DWORD cbName) {   char szBuff[MAX_NAME];   #ifdef _DEBUG   // 探測   memset(szBuff, 0x42, cbName);#endif   // 複製並使用 szName   if (cbName < MAX_NAME)      strncpy(szBuff,szName,MAX_NAME-1);}

  此代碼將嘗試向目標緩衝區寫入值 0x42。您可能會想,為什麼要這樣做而不是直接複製緩衝區呢?通過向目標緩衝區的末尾寫入一個固定的已知值,可以在源緩衝區太大時,強制代碼失敗。同時這樣也可以在開發過程中及早發現開發錯誤。與其運行攻擊者的惡意有效代碼,還不如讓程式失敗。這就是不複製攻擊者的緩衝區的原因。

注意:您只能在調試版中這樣做,以便在測試過程中擷取緩衝區溢出。

採取防範措施

  說實話,探測雖然很有用,但它並不能使您免遭攻擊。真正安全的辦法是編寫防範性的代碼。您會注意到代碼已經具有防範性了。它將檢查進入函數的資料是否不超過內部緩衝區 szBuff 。然而,有些函數在處理或複製不可靠的資料時,如果使用不當,則會存在潛在的嚴重安全問題。這裡的關鍵是不可靠的資料。在檢查代碼的緩衝區溢位錯誤時,應跟蹤資料在代碼中的流向,並檢查各種資料假設。當您意識到有些假設不正確時,您也許會驚異於所發現的錯誤。
  需要注意的函數包括諸如 strcpy、strcat、gets 等常見函數。但也不能排除所謂的 strcpy 和 strcat 的“安全的 n 版本”- strncpy 和 strncat。這些函數被認為使用起來更安全、可靠,因為它們允許開發人員限制複製到目標緩衝區中的資料的大小。然而,開發人員在使用這些函數時也會出錯!請看以下這段代碼。您能看出其中的缺點嗎?

#define SIZE(b) (sizeof(b))char buff[128];strncpy(buff,szSomeData,SIZE(buff));strncat(buff,szMoreData,SIZE(buff));strncat(buff,szEvenMoreData,SIZE(buff));

  如果您需要提示,請注意每個字串處理函數的最後一個參數。要放棄嗎?在我給出答案之前,我經常會開玩笑說,如果您禁用“不安全”的字串處理函數,而使用較為安全的 n 版本,則恐怕您要在修複新產生的錯誤中度過您的餘生。以下便是原因所在。首先,最後那個參數不是目標緩衝區的總體大小。它是緩衝區剩餘空間的大小,代碼每次向 buff 新增內容時,buff 都會有實質的減小。第二個問題是,即使使用者傳遞了緩衝區大小,他們通常也是逐一減小的。那麼在計算字串大小時,您有沒有包含末尾的Null 字元?當我針對這個問題進行讀者調查時,通常是對半分。其中一半認為在計算緩衝區大小時確實要考慮末尾Null 字元,另外一半則不這麼認為。第三,在某些情況下,n 版本可能不會以Null 字元作為結果字串的結束字元,因此請一定要閱讀文檔。
  如果編寫 C++ 代碼,請考慮使用 ATL、STL、MFC 或者您最喜歡的字串處理類來處理字串,而不要直接處理位元組。唯一潛在的不足是可能出現效能的下降,但總的來說,大部分這些類的使用都會使代碼更加強大和可維護。

使用 /GS 進行編譯

  Visual C++ .Net 中的這個新的編譯時間選項會在某些函數的堆棧架構中插入值,有助於減少基於堆棧的緩衝區溢位的潛在弱點。請記住,此選項不會修複您的代碼,也不能刪除任何錯誤。它只是象一個棒球運動的捕手,協助您減少某些類的緩衝區溢位變為可被人利用的緩衝區溢位的潛在可能性,以免攻擊者向過程中寫入代碼並執行。可以把它視為一個很小的保險措施。請注意,對於使用 Win32 應用程式嚮導建立的新的本機 Win32 C++ 項目,將預設啟用此選項。此外,Windows .NET Server 編譯時間也使用了此選項。有關詳細資料,請參閱 Brandon Bray 的 Compiler Security Checks In Depth(英文)。

排除隱患

下面我給出了一些代碼,其中至少包含一處安全隱患。您能找出來嗎?我將在下一篇文章中公布答案!

WCHAR g_wszComputerName[INTERNET_MAX_HOST_NAME_LENGTH + 1];// 擷取伺服器名稱並將其轉換為 Unicode 字串。BOOL GetServerName (EXTENSION_CONTROL_BLOCK *pECB) {   DWORD   dwSize = sizeof(g_wszComputerName);   char    szComputerName[INTERNET_MAX_HOST_NAME_LENGTH + 1];   if (pECB->GetServerVariable (pECB->ConnID,            "SERVER_NAME",            szComputerName,            &dwSize)) {   // 其餘代碼被略去

Michael Howard 是 Microsoft Secure Windows Initiative 小組的安全程式經理,也是《Writing Secure Code》的作者之一。他的主要工作就是確保人們設計、構建、測試和記錄無缺陷的安全系統。他最喜歡的話是“尺有所短,寸有所長”。

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在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.