Michael Howard 和Keith Brown
本文假設您熟悉C++、C# 和SQL
摘要:涉及安全問題時,有很多情況都會導致麻煩。您可能信任所有在您的網路上運行的程式碼,賦予所有使用者存取重要檔案的權限,並且從不費神檢查您機器上的程式碼是否已改變。您也可能沒有安裝防毒軟體,沒有給自己的程式碼建立安全機制,並賦予太多帳戶太多的權限。您甚至可能非常大意地使用大量內建函數從而允許惡意侵入,並且可能任憑伺服器連接埠開啟而沒有任何監控措施。顯然,我們還可以舉出更多的例子。哪些是真正重要的問題(即,為了避免危及您的資料和系統,應立即予以關注的最危險的錯誤)?安全專家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: Safer String 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'>
那麼您的運行環境將有兩種不同的安全環境:您將具有一個進程標記和一個執行緒標記,一般來說,執行緒標記會被用來做存取檢查(見圖3)。假設您正在編寫一個在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 的基本標誌性要求之一(請參閱http://www.microsoft.com/winlogo [英文])。
我們聽到太多的程式設計師給出藉口說他們為什麼在開發程式碼時選擇作為管理員身份運行。如果我們繼續忽略這個問題,只會讓事情變得更糟。朋友們,編輯一個文字檔案並不需要管理員權限。編輯或調試一個程式也不需要管理員權限。當您需要管理員權限時,請使用作業系統的RunAs 功能來執行緎com.asp?TARGET=/winlogo/">http://www.microsoft.com/winlogo [英文])。
我們聽到太多的程式設計師給出藉口說他們為什麼在開發程式碼時選擇作為管理員身份運行。或偵錯程式也不需要管理員權限。專欄)。目標,我們必須從根本上發生變更
。請看看Michael 的著作Writing Secure Code (Microsoft Press, 2001),這本書提供了有關如何編寫在非管理員環境下能夠很好運行的應用程式的技巧。