程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> 關於C++ >> 幫助程序員解脫困境的十條技巧

幫助程序員解脫困境的十條技巧

編輯:關於C++

安全專家Michael Howard和Keith Brown提出了十條技巧來幫助您解脫困境。

安全問題涉及許多方面。安全風險可能來自任何地方。您可能編寫了無效的錯誤處理代碼,或者在賦予權限時過於慷慨。您可能忘記了在您的服務器上正在運行什麼服務。您可能接受了所有用戶輸入。如此等等。為使您在保護自己的計算機、網絡和代碼方面有個良好開端,這裡展示了十條技巧,遵循這些技巧可以獲得一個更安全的網絡策略。

1. 信任用戶的輸入會將自己置於險境

即使不閱讀余下的內容,也要記住一點,“不要信任用戶輸入”。如果您總是假設數據是有效的並且沒有惡意,那麼問題就來了。大多數安全薄弱環節都與攻擊者向服務器提供惡意編寫的數據有關。

信任輸入的正確性可能會導致緩沖區溢出、跨站點腳本攻擊、SQL 插入代碼攻擊等等。

讓我們詳細討論一下這些潛在攻擊方式。

2. 防止緩沖區溢出

當攻擊者提供的數據長度大於應用程序的預期時,便會發生緩沖區溢出,此時數據會溢出到內部存儲器空間。緩沖區溢出主要是一個 C/C++ 問題。它們是種威脅,但通常很容易修補。我們只看到過兩個不明顯且難以修復的緩沖區溢出。開發人員沒有預料到外部提供的數據會比內部緩沖區大。溢出導致了內存中其他數據結構的破壞,這種破壞通常會被攻擊者利用,以運行惡意代碼。數組索引錯誤也會造成緩沖區下溢和超限,但這種情況

沒那麼普遍。

請看以下 C++ 代碼片段:

void DoSomething(char *cBuffSrc, DWORD cbBuffSrc)
{
  char cBuffDest[32];
  memcpy(cBuffDest,cBuffSrc,cbBuffSrc);
}

問題在哪裡?事實上,如果 cBuffSrc 和 cbBuffSrc 來自可信賴的源(例如不信任數據並因此而驗證數據的有效性和大小的代碼),則這段代碼沒有任何問題。然而,如果數據來自不可信賴的源,也未得到驗證,那麼攻擊者(不可信賴源)很容易就可以使cBuffSrc 比 cBuffDest 大,同時也將 cbBuffSrc 設定為比 cBuffDest 大。當 memcpy將數據復制到 cBuffDest 中時,來自 DoSomething 的返回地址就會被更改,因為cBuffDest 在函數的堆棧框架上與返回地址相鄰,此時攻擊者即可通過代碼執行一些惡意操作。

彌補的方法就是不要信任用戶的輸入,並且不信任 cBuffSrc 和 cbBuffSrc 中攜帶的任何數據:

void DoSomething(char *cBuffSrc, DWORD cbBuffSrc)
{
  const DWORD cbBuffDest = 32;
  char cBuffDest[cbBuffDest];
  #ifdef _DEBUG
   memset(cBuffDest, 0x33, cbBuffSrc);
  #endif
  memcpy(cBuffDest, cBuffSrc, min(cbBuffDest, cbBuffSrc));
}

此函數展示了一個能夠減少緩沖區溢出的正確編寫的函數的三個特性。首先,它要求調用者提供緩沖區的長度。當然,您不能盲目相信這個值!接下來,在一個調試版本中,代碼將探測緩沖區是否真的足夠大,以便能夠存放源緩沖區。如果不能,則可能觸發一個訪問沖突並把代碼載入調試器。在調試時,您會驚奇地發現竟有如此多的錯誤。最後也是最重要的是,對 memcpy 的調用是防御性的,它不會復制多於目標緩沖區存放能力的數據。

在 Windows Security Push at Microsoft(Microsoft Windows? 安全推動活動)中,我們為 C 程序員創建了一個安全字符串處理函數列表。您可以在 Strsafe.h: SaferString Handling in C(英文)中找到它們。

3. 防止跨站點腳本

跨站點腳本攻擊是Web特有的問題,它能通過單個Web頁中的一點隱患危害客戶端的數據。想像一下,下面的ASP.NET代碼片段會造成什麼後果:

<script language=c#>
Response.Write("您好," + Request.QueryString("name"));
</script>

有多少人曾經見過類似的代碼?但令人驚訝的是它有問題!通常,用戶會使用類似如下的URL訪問這段代碼: http://explorationair.com/welcome.aspx?name=Michael

該C#代碼認為數據始終是有效的,並且只是包含了一個名稱。但攻擊者會濫用這段代碼,將腳本和HTML代碼作為名稱提供。如果輸入如下的 URL http://northwindtraders.com/welcome.aspx?name=<script>alert('您好!');</script>

您將得到一個網頁,上面顯示一個對話框,顯示“您好!”。您可能會說,“那又怎樣?”想像一下,攻擊者可以誘導用戶點擊這樣的鏈接,但查詢字符串中卻包含一些真正危險的腳本和HTML,由此會得到用戶的 cookie 並把它發送到攻擊者擁有的網站;現在攻擊者便獲得了您的私人cookie 信息,或許會更糟。

要避免這種情況,有兩種方法。第一種是不信任輸入,並嚴格限制用戶名所包含的內容。例如,可以使用正則表達式檢查該名稱是否只包含一個普通的字符子集,並且不太大。以下 C# 代碼片段顯示了完成這一步驟的方法:

Regex r = new Regex(@"^[\w]{1,40}$");
if (r.Match(strName).Success)
{
  // 好!字符串沒問題
}
else
{
  // 不好!字符串無效
}

這段代碼使用正則表達式驗證一個字符串僅包含 1 到 40 個字母或數字。這是確定一個值是否正確的唯一安全方法。

HTML 或腳本不可能蒙混過此正則表達式!不要使用正則表達式尋找無效字符並在發現這種無效字符後拒絕請求,因為容易出現漏掉的情況。第二種防范措施是對所有作為輸出的輸入進行HTML編碼。這會減少危險的HTML標記,使之變成更安全的轉義符。您可以在 ASP.NET 中使用 HttpServerUtility.HtmlEncode,或者在 ASP 中使用Server.HTMLEncode 轉義任何可能出現問題的字符串。

4. 不要請求 sa 權限

我們要討論的最後一種輸入信任攻擊是 SQL 插入代碼。許多開發人員編寫這樣的代碼,即獲取輸入並使用該輸入來建立 SQL 查詢,進而與後台數據存儲(如 Microsoft SQL Server 或 Oracle)進行通信。

請看以下代碼片段:

void DoQuery(string Id)
{
  SqlConnection sql=new SqlConnection(@"data source=localhost;" + "user id=sa;password=password;");
  sql.Open();
  sqlstring= "SELECT hasshipped" + " FROM shipping WHERE id='" + Id + "'";
  SqlCommand cmd = new SqlCommand(sqlstring,sql);
  ...
}

這段代碼有三個嚴重缺陷。首先,它是以系統管理員帳戶 sa 建立從Web服務到SQL Server的連接的。不久您就會看到這樣做的缺陷所在。第二點,注意使用“password”作為 sa帳戶密碼的聰明做法!但真正值得關注的是構造 SQL 語句的字符串連接。如果用戶為 ID 輸入 1001,您會得到如下 SQL 語句,它是完全有效的。

SELECT hasshipped FROM shipping WHERE id = '1001'

但攻擊者比這要有創意得多。他們會為 ID 輸入一個“'1001' DROP table shipping --”,它將執行如下查詢:

SELECT hasshipped FROM shipping WHERE id = '1001' DROP table shipping -- ';

它更改了查詢的工作方式。這段代碼不僅會嘗試判斷是否裝運了某些貨物,它還會繼續 drop(刪除)shipping 表!操作符 -- 是 SQL 中的注釋操作符,它使攻擊者能夠更容易地構造一系列有效但危險的 SQL 語句!

這時您也許會覺得奇怪,怎麼任何一個用戶都能刪除 SQL Server 數據庫中的表呢。當然,您是對的,只有管理員才能做這樣的工作。但這裡您是作為 sa 連接到數據庫的,而 sa 能在 SQL Server 數據庫上做他想做的任何事。永遠不要在任何應用程序中以 sa連接 SQL Server;正確的做法是,如果合適,使用 Windows 集成的身份驗證,或者以一個預先定義的具有適當權限的帳戶連接。

修復 SQL 插入代碼問題很容易。使用 SQL 存儲過程及參數,下面的代碼展示了創建這種查詢的方法 - 以及如何使用正則表達式來確認輸入有效,因為我們的交易規定貨運ID 只能是 4 到 10 位數字:

Regex r = new Regex(@"^\d{4,10}$");
if (!r.Match(Id).Success)
  throw new Exception("無效 ID");
SqlConnection sqlConn= new SqlConnection(strConn);
string str="sp_HasShipped";
SqlCommand cmd = new SqlCommand(str,sqlConn);
cmd.CommandType = CommandType.StoredProcedure;
cmd.Parameters.Add("@ID",Id);

緩沖區溢出、跨站點腳本和 SQL 插入代碼攻擊都是信任輸入問題的示例。所有這些攻擊都能通過一種機制來減輕危害,即認為所有輸入都是有害的,除非獲得證明。

5. 注意加密代碼!

下面我們來看些會讓我們吃驚的東西。我發現我們檢查的安全代碼中百分之三十以上都存在安全漏洞。最常見的漏洞可能就是自己的加密代碼,這些代碼很可能不堪一擊。永遠不要創建自己的加密代碼,那是徒勞的。不要認為僅僅因為您有自己的加密算法其他人就無法破解。攻擊者能使用調試器,他們也有時間和知識來確認系統如何工作 - 通常在幾小時內就會破解它們。您應該使用 Win32? 的CryptoAPI,system.Security.Cryptography 命名空間提供了大量優秀且經過測試的加密算法。

6. 減少自己被攻擊的可能性

如果沒有百分之九十以上的用戶要求,則不應默認安裝某一功能。Internet Information Services (IIS) 6.0 遵循了這一安裝建議,您可以在這個月發布的 Wayne Berry 的文章“Innovations in Internet Information Services Let You Tightly Guard Secure Data and Server Processes”中讀到相關內容。這種安裝策略背後的思想是您不會注意自己並未使用的服務,如果這些服務正在運行,則可能被其他人利用。如果默認安裝某功能,則它應在最小授權原則下運行。也就是說,除非必要,否則不要允許使用管理員權限運行應用程序。最好遵循這一忠告。

7. 使用最小授權原則

出於若干原因,操作系統和公共語言運行時有一個安全策略。很多人以為此安全策略存在的主要原因是防止用戶有意破壞:訪問他們無權訪問的文件、重新配置網絡以達到他們的要求以及其他惡劣行為。的確,這種來自內部的攻擊很普遍,也需要防范,但還有另一個原因需要嚴守這一安全策略。即在代碼周圍建立起防范壁壘以防止用戶有意或(正如經常發生的)無意的操作對網絡造成嚴重破壞。例如,通過電子郵件下載的附件在Alice

的機器上執行時被限制為只能訪問 Alice 可以訪問的資源。如果附件中含有特洛伊木馬,那麼好的安全策略就是限制它所能產生的破壞。當您設計、建立並部署服務器應用程序時,您不能假設所有請求都來自合法用戶。如果一個壞家伙發送給您一個惡意請求(但願不會如此)並使您的代碼產生惡劣操作,您會希望您的應用程序擁有所有可能的防護來限制損害。因此我們認為,您的公司實施安全策略不僅是因為它不信任您或您的代碼,同時也是為了保護不受外界有企圖的代碼的傷害。

最小授權原則認為,要在最少的時間內授予代碼所需的最低權限。也就是說,任何時候都應在您的代碼周圍豎起盡可能多的防護牆。當發生某些不好的事情時 - 就象 Murphy定律保證的那樣 - 您會很高興這些防護牆都處在合適的位置上。因此,這裡就使用最小授權原則運行代碼給出了一些具體方法。

為您的服務器代碼選擇一個安全環境,僅允許其訪問完成其工作所必需的資源。如果您代碼中的某些部分要求很高的權限,請考慮將這部分代碼分離出來並單獨以較高的權限運行。為安全分離這一以不同的操作系統驗證信息運行的代碼,您最好在一個單獨的進程(運行在具有更高權限的安全環境中)中運行此代碼。這意味著您將需要進程間通訊(如COM 或 Microsoft .NET 遠程處理),並且需要設計該代碼的接口以使往返行程最小。

如果在 .NET Framework 環境中將代碼分離成程序集,請考慮每段代碼所需的權限級別。您會發現這是一個很容易的過程:把需要較高權限的代碼分離到可賦予其更多權限的單獨的程序集中,同時使其余大部分程序集以較低的權限運行,從而在您的代碼周圍添加更多的防護。 在進行此操作時,不要忘了,由於代碼訪問安全 (CAS) 堆棧的作用,您限制的不僅是自己程序集的權限,也包括您調用的任何程序集的權限。

許多人建立了自己的應用程序,使得其產品在測試並提供給客戶後可以插入新的組件。保護這種類型的應用程序非常困難,因為您無法測試所有可能的代碼路徑來發現錯誤和安全漏洞。然而,如果您的應用程序是托管的,則 CLR 提供了一個極好的功能,可以使用它關閉這些可擴展點。通過聲明一個權限對象或一個權限集並調用 PermitOnly 或Deny,您可以為自己的堆棧添加一個標記,它將阻塞授予您調用的任何代碼的權限。通過在調用某個插件之前進行此操作,您就可以限制該插件所能執行的任務。例如,一個用於計算分期付款的插件不需要任何訪問文件系統的權限。這只是最小權限的另一個例子,由此您可以事先保護自己。請確保記錄下這些限制,並注意,具有較高權限的插件能夠使用Assert 語句逃避這些限制。

8. 注意失敗模式

接受它吧。其他人和您一樣憎恨編寫錯誤處理代碼。導致代碼失敗的原因如此眾多,一想到這些就讓人沮喪。大多數程序員,包括我們,更願意關注正常的執行路徑。那裡才是真正完成工作的地方。讓我們盡可能快而無痛地完成這些錯誤處理,然後繼續下一行真正的代碼吧。

只可惜,這種情緒並不安全。相反,我們需要更密切地關注代碼中的失敗模式。人們對這些代碼的編寫通常很少深入注意,並且常常沒有經過完全測試。還記得最後一次您完全肯定調試過函數的每一行代碼,包括其中每一個很小的錯誤處理程序是什麼時候?

未經測試的代碼常會導致安全漏洞。有三件事情可以幫助您減輕這個問題。首先,對那些很小的錯誤處理程序給予和正常代碼同樣的關注。考慮當您的錯誤處理代碼執行時系統的狀態。系統是否處於有效並且安全的狀態中?其次,一旦您編寫了一個函數,請逐步將它徹底調試幾遍,確保測試每一個錯誤處理程序。注意,即使使用這樣的技術,也可能無法發現非常隱秘的計時錯誤。您可能需要給您的函數傳遞錯誤參數,或者以某種方式調整系統的狀態,以使您的錯誤處理程序得以執行。通過花時間單步調試代碼,您可以慢下來並有足夠的時間來查看代碼以及系統運行時的狀態。通過在調試器中仔細單步執行代碼,我們在自己的編程邏輯中發現了許多缺陷。這是一個已得到證明的技術。請使用這一技術。最後,確保您的測試組合能使您的函數進行失敗測試。盡量使測試組合能夠檢驗函數中的每一行代碼。這能幫助您發現規律,特別是當使測試自動化並在每次建立代碼後運行測試時。關於失敗模式還有一件非常重要的事情需要說明。當您的代碼失敗時要確保系統處於可能的最安全狀態。下面顯示了一些有問題的代碼:

bool accessGranted = true; // 過於樂觀!
try
{
  // 看看我們能否訪問 c:\test.txt
  new FileStream(@"c:\test.txt", FileMode.Open, FileAccess.Read).Close();
}
catch (SecurityException x)
{
  // 訪問被拒絕
  accessGranted = false;
}
catch (...)
{
  // 發生了其他事情
}

盡管我們使用了 CLR,我們仍被允許訪問該文件。在這種情況下,並沒有引發一個SecurityException。但是,例如,如果文件的自由訪問控制列表 (DACL) 不允許我們訪問呢?這時,會引發另一種類型的異常。但由於代碼第一行的樂觀假設,我們永遠也不會知道這一點。編寫這段代碼的一種更好的方法就是持謹慎態度:

bool accessGranted = false; // 保持謹慎!
try
{
  // 看看我們能否訪問 c:\test.txt
  new FileStream(@"c:\test.txt",FileMode.Open,FileAccess.Read).Close();
  // 如果我們還在這裡,那麼很好!
  accessGranted = true;
}
catch (...) {}

這樣會更加穩定,因為無論我們如何失敗,總會回到最安全的模式。

9. 模擬方式非常容易受到攻擊

編寫服務器應用程序時,您常常會發現自己直接或間接使用了 Windows 的一個稱為模擬的很方便的功能。模擬允許進程中的每個線程運行在不同的安全環境中,通常是客戶端的安全環境。例如,當文件系統重定向器通過網絡收到一個文件請求時,它對遠程客戶端進行身份驗證,檢查以確認客戶端的請求沒有違反共享上的 DACL,然後把客戶端的標記附加到處理請求的線程上,從而模擬客戶端。然後此線程便可以使用客戶端的安全環境訪問服務器上的本地文件系統。由於本地文件系統已經是安全的,因此這樣做很方便。它會考慮所請求的訪問類型、文件上的 DACL 和線程上的模擬標記來進行一個訪問檢查。如果訪問檢查失敗,本地文件系統會將其報告給文件系統重定向器,然後重定向器向遠程客戶端發送一個錯誤。毫無疑問,對文件系統重定向器來說這很方便,因為它只是簡單地把請求傳給本地文件系統,讓它去做自己的訪問檢查,就好象客戶端在本地一樣。這對於文件重定向器這樣簡單的網關而言,一切良好。但模擬常常用在其他更復雜的應用程序中。以一個Web應用程序為例。如果您編寫一個經典的非托管 ASP 程序、ISAPI 擴展或ASP.NET 應用程序,在它的Web.config 文件中有如下指定

<identity impersonate='true'>

那麼您的運行環境將有兩種不同的安全環境:您將具有一個進程標記和一個線程標記,一般來說,線程標記會被用來做訪問檢查(見圖)。假設您正在編寫一個在Web服務器進程中運行的 ISAPI 應用程序,並假定大多數請求未經身份驗證,則您的線程標記可能是 IUSR_MACHINE,而進程標記卻是 SYSTEM!假設您的代碼能被一個壞家伙通過緩沖區溢出利用。您認為他會只滿足作為 IUSR_MACHINE 運行嗎?當然不會。他的攻擊代碼很可能會調用 RevertToSelf 以刪除模擬標記,從而希望提高他的權限級別。在這種情況下,他會很容易獲得成功。他還可以調用 CreateProcess。它不會從模擬標記復制新進程的標記,而是從進程標記復制,這樣新進程便可以作為 SYSTEM 運行。

那麼怎樣解決這個小問題呢?除了首先確保不出現任何緩沖區溢出外,還要記住最小授權原則。如果您的代碼不需要具有 SYSTEM 這樣大的權限,則不要將Web應用程序配置為在Web服務器進程中運行。如果只是將Web應用程序配置為在中等或較高的隔離環境中運行,您的進程標記將會是 IWAM_MACHINE。您實際上沒有任何權限,因而這種攻擊幾乎不會生效。注意,在 IIS 6.0(即將成為 Windows .NET Server 的一個組件)中,默認情況下用戶編寫的代碼不會作為 SYSTEM 運行。基於這樣的認識,即開發人員確實會犯錯誤,Web 服務器就減少賦予代碼的權限而提供的任何幫助都是有益的,以免萬一代碼中存在安全問題。

下面是另外一個 COM 程序員可能遇到的隱患。COM 有一個不好的傾向就是敷衍線程。如果您調用一個進程內 COM 服務器,而其線程模型與調用線程的模型不匹配,則 COM會在另一個線程上執行調用。COM 不會傳播調用者線程上的模擬標記,這樣結果就是調用會在進程的安全環境中執行,而不是在調用線程的安全環境中。多麼令人吃驚!

下面是另一個由模擬帶來的隱患的情況。假設您的服務器接受通過命名管道、DCOM或 RPC 發送的請求。您對客戶端進行身份驗證並模擬它們,通過模擬以它們的名義打開內核對象。而您又忘了在客戶端斷開連接時關閉其中的一個對象(例如一個文件)。當下一個客戶端進入時,您又對其進行身份驗證和模擬,猜猜會發生什麼?您仍然可以訪問上一個客戶端“遺漏”的文件,即使新的客戶端並沒有獲得訪問該文件的權限。出於運行性能的原因,內核僅在第一次打開對象時對其執行訪問檢查。即使您後來因為模擬其他用戶而更改了安全環境,您還是可以訪問此文件。以上提及的這些情況都是為了提醒一點,即模擬為服務器開發人員提供了方便,但這種方便卻具有很大隱患。在您采用一個模擬標記運行程序時,務必要對自己的代碼多加注意。

10. 編寫非管理員用戶可以實際使用的應用程序

這確實是最小授權原則的必然結果。如果程序員繼續開發這樣的代碼,使得必須是管理員身份的用戶才能在 Windows上正常運行,我們就不能期望提高系統的安全性。Windows 有一套非常穩定的安全功能,但是如果用戶必須具有管理員身份才能進行操作,他們就不能很好地利用這些功能。

您怎樣進行改進呢?首先,自己先嘗試一下,不以管理員身份運行。您很快就會知道使用沒有考慮安全設計的程序的痛苦。有一天,我 (Keith) 安裝一個由手持設備制造商提供的軟件,該軟件用於在我的台式機和手持設備之間同步數據。與往常一樣,我退出了普通的用戶帳戶,然後使用內置的管理員帳戶再次登錄,安裝了軟件,然後再次登錄到普通帳戶,並且試圖運行軟件。結果該應用程序跳出一個對話框,說不能訪問某個所需的數據文件,接著便給出一個訪問沖突信息。朋友們,這就是某個主流手持設備廠商的軟件產品。對這種錯誤還有什麼借口嗎?

在運行了來自 http://sysinternals.com(英文)的 FILEMON 之後,我很快發現該應用程序試圖打開一個數據文件以進行寫入訪問,而該文件與應用程序的可執行文件安裝在同一目錄中。當應用程序如預想的那樣安裝在 Program Files 目錄中時,他們絕不能試圖向該目錄寫入數據。Program Files 具有這樣一個限制訪問控制策略是有原因的。我們不希望用戶寫入這些目錄,因為這樣會很容易讓一個用戶留下特洛伊木馬程序,而讓另一個用戶去執行。實際上,這個約定是 Windos XP 的基本標志性要求之一。

我們聽到太多的程序員給出借口說他們為什麼在開發代碼時選擇作為管理員身份運行。如果我們繼續忽略這一問題,只會讓事情更糟。朋友們,編輯一個文本文件並不需要管理員權限。編輯或調試一個程序也不需要管理員權限。在您需要管理員權限時,請使用操作系統的 RunAs 功能來運行具有較高權限的單獨的程序。如果您是在編寫給開發人員使用的工具,那麼您將對這個群體負有額外的責任。我們需要停止這種編寫只有以管理員身份才能運行的代碼的惡性循環,要達到這一目標,我們必須從根本上發生改變。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved