從具體上來說,.NET元數據機制的設計,既方便了反射等強大特性的實現,又同時給代碼安全及程序運行時安全帶來了巨大的隱患。迄今為止,還未發現比較有效元數據可見性控制方法。當然,這不在本文的討論范圍之內。我還是更願意在這篇文章在針對.NET的內存分配機制討論一個更具體的問題:如何保護在內存中存儲的敏感數據?
String的駐留機制帶來的安全性問題
String是代碼中使用頻率很高的對象類型。為了提高字符串的處理速度,節省內存空間,Microsoft為.NET String類設計了駐留機制。其大概的邏輯模型是,大部分String存儲在一個類似的Hash Table中,string的內容是哈希表的key,該key對應的value是string的內存地址。這樣內容相同的string實際上只是對應內存堆上同一個字符串。之所以說是大部分而不是全部,是因為有一部分動態創建(concat)的string,是不會進入這樣一個虛擬的hash Table中的。本文的最後附上String類的源代碼,有興趣的同學可以研究研究。
這就帶來了最主要的問題,你無法准確控制或者預測一個特定字符串的生命周期。一個以string形式呈現的敏感數據(比如密碼)很有可能在內存中一直存在,而你卻預測它在超出某個特定函數的作用域的時候就被垃圾回收了。這樣,當發生操作系統換頁的時候(而這也往往是可能發生的),這個敏感數據就被保存到本地文件pagefile。sys當中,或者當操作系統休眠的時候,敏感數據進入hiberfil.sys中。一個可能的敏感數據洩漏過程是:
使用SecureString類
現在既然String靠不住了,我們能有什麼簡單的方法來特別的保護我的敏感數據嗎? 幸運的是,.NET從Version 2.0開始,為我們提供了一套基於DPAPI的解決方法 - SecureString。
SecureString類具有以下特性:
SecureString中的內容是加密之後的,而不是平文;
使用Windows的加密方案DPAPI ;
SecureString只能在基於NT的平台上使用
C#代碼示例:
public void MethodA() { //using DPAPI to encrpt the sensitive content System.Security.SecureString password = new System.Security.SecureString(); char[] pass = { 'p','a','s','s','w','o','r','d' }; for (int i = 0;i < pass.Length;i++) { password.AppendChar(pass[i]); } password.MakeReadOnly(); //pass the encrypted password through memory or file } public void MethodB(System.Security.SecureString password) { string decryptedPassword = ""; //copy the secure content to a long pointer IntPtr ptr = System.Runtime.InteropServices.Marshal.SecureStringToBSTR(password); try { //Convert secure content to string using DPAPI decryptedPassword = System.Runtime.InteropServices.Marshal.PtrToStringBSTR(ptr); //using the decrypted password to check } finally { System.Runtime.InteropServices.Marshal.ZeroFreeBSTR(ptr); password.Dispose(); } }
這段代碼中有幾個值得說明的地方:
代碼寫得有些粗糙,僅為示意。
使用Char數組來保存敏感數據的原始值。 因為Char數組的生命周期是可以預期的,它在超出自己的作用域之後,就被回收。
MakeReadOnly方法,一旦使用了該方法後,SecureString的內容就不能再被修改,從而保證了加密後的數據不能再被修改,否則將引發異常。
SecureString的解密,是通過將其內容復制到一個長指針中,然後利用DPAPI,最終獲得String。該String不會進入上文所說的那個虛擬Hash Table中。
ZeroFreeBSTR()方法。因為使用COM Interop引入了非托管資源,所以一定不能忘記使用ZeroFreeBSTR來釋放指針,否則會造成內存洩漏。
SecureString類重寫了基類的ToString()方法,不過該方法不會返回所持有的加密內容,而總是返回System.Security.SecureString。
敏感數據已經足夠安全了嗎?
這個問題的答案很讓我們沮喪,不是。有兩個問題:
用戶的輸入往往先被處理成string,然後才能傳遞到我們的處理函數,比如command line parameters,或者textbox。
.NET Framework的很多函數都要求string參數,而非SecureString,比如ADO.NET的Connect函數。
幸運的是,對於這兩個問題,我們除了祈禱Microsoft盡快更新Framework以外,在當前條件下還有些辦法來處理。
針對第一個問題,重寫Command Line或者Textbox,添加對SecureString的支持。
針對第二個問題,利用GC特性來處理。
第二個問題的主要安全隱患是來自於string的特性,即不可變性(immutable)。為了防止GC的自作聰明處理我們的數據,從而造成敏感數據洩漏,我們需要對GC做一些處理,此時上面代碼的MethodB就應該修改成如下:
public unsafe void MethodB(System.Security.SecureString password) { int pwdLength = password.Length; IntPtr passwordPtr = IntPtr.Zero; //allocate a pinned memory to store the password in string form string decryptedPassword = new string('',pwdLength); GCHandle gch = GCHandle.Alloc(decryptedPassword,GCHandleType.Pinned); try { //copy the secure content to a long pointer passwordPtr = System.Runtime.InteropServices.Marshal.SecureStringToBSTR(password); var pPassword = (char*)passwordPtr; var pDecryptedPassword = (char*)gch.AddrOfPinnedObject(); for (int index = 0; index < pwdLength; index++) { pDecryptedPassword[index] = pPassword[index]; } } finally { if (IntPtr.Zero != passwordPtr) { System.Runtime.InteropServices.Marshal.ZeroFreeBSTR(passwordPtr); } } }
我們用GCHandleType。Pinned標志,申請了一塊固定位置的內存來存儲密碼,這段明文密碼是獨立於string類的虛擬hash table的。這可以在一定程度上減少因不當權限訪問造成的敏感數據洩露。
到這裡,string是可以用了,但是換頁的問題還沒有解決啊?
是的,你可能已經覺得麻煩了。我們不得已而為之,實在是因為.NET Framework對於SecureString的支持還不夠完善,或者說是部分的。上面雖然解決了String的不可變特性造成的問題,但是重新引入系統換頁的問題。怎麼辦?
在這種情況下,我們只能求助於Windows API。Windows API對於頁的操作為我們提供了2個接口:
AllocateuserPhysicalPages 和VirtualLock,這兩個函數可以將我們在上例中所取得的密碼存儲地址pDecryptedPassword 鎖定在內存中,強制不換頁。不過這麼做要萬分小心,因為一旦pDecryptedPassword 所指向的密碼內容被強制不換頁,那該程序的整個workset都會一直被強制在內存中,一直到程序結束。這可能給系統的其他程序帶來糟糕的體驗。
關於使用VirtualLock來強制Page In的修改,就不再討論了。
總結
事物總是兩面性的,.NET給我們帶來了快速實現,關注業務的好處,卻缺少了譬如C++般精確操作內存這樣的靈活性,因而在安全性方面如果Framework不夠完善,我們就會多多少少有些掣肘。總之,在現有條件下,盡力實現系統安全性,是我們的目標。本文沒有討論系統設計的安全性考慮等這些概念性理論性的東西,而是從最具體的String類入手討論,希望對您有一些啟發。