[英]Is SecureString ever practical in a C# application?
如果我的假設在這里有誤,請隨時糾正我,但讓我解釋一下我為什么要問。
取自 MSDN,一個SecureString
:
表示應該保密的文本。 文本在使用時加密以保護隱私,並在不再需要時從計算機內存中刪除。
我明白了,通過System.String
將密碼或其他私人信息存儲在SecureString
是完全有意義的,因為您可以控制它實際存儲在內存中的方式和時間,因為System.String
:
是不可變的,當不再需要時,不能以編程方式安排垃圾收集; 也就是說,實例在創建后是只讀的,無法預測實例何時會從計算機內存中刪除。 因此,如果 String 對象包含諸如密碼、信用卡號或個人數據之類的敏感信息,則存在信息在使用后可能會泄露的風險,因為您的應用程序無法從計算機內存中刪除數據。
但是,對於 GUI 應用程序(例如,ssh 客戶端),必須從System.String
構建SecureString
。 所有文本控件都使用字符串作為其基礎數據類型。
因此,這意味着每次用戶按下一個鍵時,舊字符串都會被丟棄,並且會構建一個新字符串來表示文本框中的值,即使使用密碼掩碼也是如此。 而且我們無法控制何時或是否從內存中丟棄這些值中的任何一個。
現在是登錄服務器的時候了。 你猜怎么着? 您需要通過連接傳遞一個字符串以進行身份驗證。 因此,讓我們將SecureString
轉換為System.String
.... 現在我們在堆上有一個字符串,無法強制它進行垃圾收集(或將 0 寫入其緩沖區)。
我的觀點是:無論你做什么,沿着這條線的某個地方, SecureString
將被轉換成System.String
,這意味着它至少會在某個時候存在於堆上(沒有任何垃圾收集保證)。
我的觀點不是:是否有辦法繞過將字符串發送到 ssh 連接,或者繞過讓控件存儲字符串(制作自定義控件)。 對於這個問題,您可以將“ssh 連接”替換為“登錄表”、“注冊表”、“付款表”、“foods-you-would-feed-your-puppy-but-not-your-children 表”,等等。
SecureString
真正變得實用呢?System.String
對象的使用?SecureString
的全部意義在於簡單地減少System.String
在堆上的時間(降低其移動到物理交換文件的風險)?SecureString
阻止他獲得數據呢?對不起,如果我把問題問得太厚了,好奇心占了上風。 隨時回答我的任何或所有問題(或告訴我我的假設完全錯誤)。 :)
SecureString
實際上有非常實際的用途。
你知道我見過多少次這樣的場景嗎? (答案是:很多!):
RedGate
軟件,它可以在異常情況下捕獲局部變量的“值”,非常有用。 不過,我可以想象它會意外地記錄“字符串密碼”。 你知道如何避免所有這些問題嗎? SecureString
。 它通常可以確保您不會犯這樣的愚蠢錯誤。 它是如何避免的? 通過確保密碼在非托管內存中加密,並且只有當您對自己在做什么有 90% 的把握時才能訪問真正的值。
從某種意義上說, SecureString
很容易工作:
1)一切都是加密的
2)用戶調用AppendChar
3)解密UNMANAGED MEMORY中的所有內容並添加字符
4) 再次加密 UNMANAGED MEMORY 中的所有內容。
如果用戶可以訪問您的計算機怎么辦? 病毒是否能夠訪問所有SecureStrings
? 是的。 你需要做的就是在內存被解密的時候把自己掛在RtlEncryptMemory
,你會得到未加密的內存地址的位置,並讀出它。 瞧! 事實上,您可以制作一種病毒,它會不斷掃描SecureString
使用情況並記錄所有使用它的活動。 我並不是說這將是一項容易的任務,但它是可以完成的。 如您所見,一旦您的系統中存在用戶/病毒, SecureString
的“強大”就完全消失了。
你在你的帖子中有幾點。 當然,如果您使用一些在內部保存“字符串密碼”的 UI 控件,那么使用實際的SecureString
就沒有那么有用了。 盡管如此,它仍然可以防止我上面列出的一些愚蠢行為。
此外,正如其他人所指出的,WPF 支持 PasswordBox,它通過其SecurePassword屬性在內部使用SecureString
。
底線是; 如果您有敏感數據(密碼、信用卡等),請使用SecureString
。 這就是 C# 框架所遵循的。 例如, NetworkCredential
類將密碼存儲為SecureString
。 如果你看看這個,你會發現SecureString
.NET 框架中有超過 80 多種不同的用法。
在很多情況下,您必須將SecureString
轉換為字符串,因為某些 API 需要它。
通常的問題是:
您提出了一個好觀點:當SecureString
轉換為string
時會發生什么? 這只能因為第一點而發生。 例如,API 不知道它是敏感數據。 我個人沒有看到這種情況發生。 從 SecureString 中取出字符串並不是那么簡單。
這並不簡單,原因很簡單; 從來沒有打算讓用戶將 SecureString 轉換為字符串,正如您所說:GC 將啟動。如果您看到自己這樣做,您需要退后一步問問自己:我為什么要這樣做,或者我真的需要這,為什么?
我看到了一個有趣的案例。 即,WinApi 函數 LogonUser 以 LPTSTR 作為密碼,這意味着您需要調用SecureStringToGlobalAllocUnicode
。 這基本上為您提供了位於非托管內存中的未加密密碼。 完成后,您需要立即擺脫它:
// Marshal the SecureString to unmanaged memory.
IntPtr rawPassword = Marshal.SecureStringToGlobalAllocUnicode(password);
try
{
//...snip...
}
finally
{
// Zero-out and free the unmanaged string reference.
Marshal.ZeroFreeGlobalAllocUnicode(rawPassword);
}
您始終可以使用擴展方法擴展SecureString
類,例如ToEncryptedString(__SERVER__PUBLIC_KEY)
,它為您提供使用服務器公鑰加密的SecureString
的string
實例。 只有服務器才能解密它。 問題已解決:垃圾收集永遠不會看到“原始”字符串,因為您永遠不會在托管內存中公開它。 這正是PSRemotingCryptoHelper
( EncryptSecureStringCore(SecureString secureString)
) 中EncryptSecureStringCore(SecureString secureString)
。
作為一些非常相關的東西: Mono SecureString 根本不加密。 該實現已被注釋掉,因為..等待它.. “它以某種方式導致 nunit 測試損壞” ,這帶來了我的最后一點:
並非所有地方都支持SecureString
。 如果平台/架構不支持SecureString
,您將收到異常。 文檔中提供了支持的平台列表。
您的假設中很少有問題。
首先, SecureString 類沒有 String 構造函數。 為了創建一個對象,您分配一個對象,然后附加字符。
在 GUI 或控制台的情況下,您可以非常輕松地將每個按下的鍵傳遞給安全字符串。
該類的設計方式使您不能錯誤地訪問存儲的值。 這意味着您無法直接從中獲取string
作為密碼。
因此,例如,要使用它來通過 Web 進行身份驗證,您必須使用同樣安全的適當類。
在 .NET 框架中,您有幾個可以使用 SecureString 的類
總而言之,SecureString 類可能很有用,但需要開發人員給予更多關注。
所有這一切,以及示例,在 MSDN 的SecureString文檔中都有很好的描述
SecureString 在以下情況下很有用:
您逐個構建它(例如從控制台輸入)或從非托管 API 獲取它
您可以通過將其傳遞給非托管 API (SecureStringToBSTR) 來使用它。
如果您曾經將其轉換為托管字符串,那么您就違背了它的目的。
更新以回應評論
... 或者像你提到的 BSTR,這似乎不再安全
將其轉換為 BSTR 后,使用 BSTR 的非托管組件可以將內存歸零。 非托管內存更安全,因為它可以通過這種方式重置。
但是,.NET Framework 中支持 SecureString 的 API 很少,所以您說它在今天的價值非常有限是正確的。
我看到的主要用例是在要求用戶輸入高度敏感的代碼或密碼的客戶端應用程序中。 可以逐個字符地使用用戶輸入來構建 SecureString,然后可以將其傳遞給非托管 API,該 API 在使用后將收到的 BSTR 歸零。 任何后續內存轉儲都不會包含敏感字符串。
在服務器應用程序中,很難看出它在哪里有用。
更新 2
接受 SecureString 的 .NET API 的一個示例是X509Certificate 類的構造函數。 如果您使用 ILSpy 或類似方法進行探索,您會看到 SecureString 在內部轉換為非托管緩沖區 ( Marshal.SecureStringToGlobalAllocUnicode
),然后在完成 ( Marshal.ZeroFreeGlobalAllocUnicode
) 后將其歸零。
Microsoft 不建議對較新的代碼使用SecureString
。
從SecureString Class 的文檔中:
重要的
我們不建議您將
SecureString
類用於新開發。 有關更多信息,請參閱不應使用SecureString
其中推薦:
不要將
SecureString
用於新代碼。 將代碼移植到 .NET Core 時,請考慮數組的內容在內存中未加密。處理憑證的一般方法是避免使用憑證,而是依靠其他方式進行身份驗證,例如證書或 Windows 身份驗證。 在 GitHub 上。
正如您已經正確識別的那樣, SecureString
提供了一個優於string
特定優勢:確定性擦除。 這個事實有兩個問題:
SecureString
的目的。 這意味着您必須小心,永遠不要創建 GC 管理的不可變string
或任何其他將存儲敏感信息的緩沖區(或者您也必須跟蹤它)。 在實踐中,這並不總是容易實現,因為許多 API 只提供一種處理string
,而不是SecureString
。 即使你確實做到了一切正確......SecureString
可以防止非常特定類型的攻擊(對於其中一些攻擊,它甚至不那么可靠)。 例如, SecureString
確實允許您縮小攻擊者可以轉儲進程內存並成功提取敏感信息的時間窗口(同樣,正如您正確指出的那樣),但希望窗口對於攻擊者來說太小對您的記憶進行快照根本不被認為是安全的。 那么,你應該什么時候使用它? 只有當您使用的東西可以讓您使用SecureString
滿足您的所有需求時,您仍然應該注意這僅在特定情況下是安全的。
以下文字復制自 HP Fortify 靜態代碼分析器
摘要: PassGenerator.cs 中的 PassString() 方法以不安全的方式(即以字符串形式)存儲敏感數據,從而可以通過檢查堆來提取數據。
說明:如果將存儲在內存中的敏感數據(例如密碼、社會保險號、信用卡號等)存儲在托管 String 對象中,則可能會泄漏。 字符串對象沒有被固定,因此垃圾收集器可以隨意重新定位這些對象並在內存中保留多個副本。 這些對象默認不加密,因此任何可以讀取進程內存的人都可以看到內容。 此外,如果進程的內存被換出到磁盤,字符串的未加密內容將被寫入交換文件。 最后,由於 String 對象是不可變的,從內存中刪除 String 的值只能由 CLR 垃圾收集器完成。 除非 CLR 內存不足,否則不需要運行垃圾收集器,因此無法保證垃圾收集何時發生。 在應用程序崩潰的情況下,應用程序的內存轉儲可能會泄露敏感數據。
建議:不要將敏感數據存儲在像字符串這樣的對象中,而是將它們存儲在 SecureString 對象中。 每個對象始終以加密格式將其內容存儲在內存中。
我想說明這一點:
如果攻擊者已經擁有進行堆檢查的方法,那么他們很可能 (A) 已經擁有讀取擊鍵的方法,或者 (B) 已經物理上擁有機器......那么使用
SecureString
阻止他們進入數據呢?
攻擊者可能無法完全訪問計算機和應用程序,但可以訪問進程內存的某些部分。 當特殊構造的輸入可能導致應用程序暴露或覆蓋內存的某些部分時,它通常是由緩沖區溢出等錯誤引起的。
以 Heartbleed 為例。 特殊構造的請求可能會導致代碼向攻擊者公開進程內存的隨機部分。 攻擊者可以從內存中提取 SSL 證書,但他唯一需要的只是使用格式錯誤的請求。
在托管代碼的世界中,緩沖區溢出成為問題的頻率要低得多。 對於 WinForms,數據已經以不安全的方式存儲,您對此無能為力。 這使得SecureString
的保護幾乎毫無用處。
但是,可以對 GUI 進行編程以使用SecureString
,在這種情況下,減少內存中的密碼可用性窗口是值得的。 例如,WPF 中的PasswordBox.SecurePassword是SecureString
類型。
前段時間我不得不針對 java 信用卡支付網關創建 ac# 接口,並且需要一個兼容的安全通信密鑰加密。 由於 Java 實現相當具體,我必須以給定的方式處理受保護的數據。
我發現這種設計非常易於使用,而且比使用 SecureString 更容易……對於那些喜歡使用的人……隨意,沒有法律限制:-)。 請注意,這些類是內部類,您可能需要將它們設為公開。
namespace Cardinity.Infrastructure
{
using System.Security.Cryptography;
using System;
enum EncryptionMethods
{
None=0,
HMACSHA1,
HMACSHA256,
HMACSHA384,
HMACSHA512,
HMACMD5
}
internal class Protected
{
private Byte[] salt = Guid.NewGuid().ToByteArray();
protected byte[] Protect(byte[] data)
{
try
{
return ProtectedData.Protect(data, salt, DataProtectionScope.CurrentUser);
}
catch (CryptographicException)//no reason for hackers to know it failed
{
#if DEBUG
throw;
#else
return null;
#endif
}
}
protected byte[] Unprotect(byte[] data)
{
try
{
return ProtectedData.Unprotect(data, salt, DataProtectionScope.CurrentUser);
}
catch (CryptographicException)//no reason for hackers to know it failed
{
#if DEBUG
throw;
#else
return null;
#endif
}
}
}
internal class SecretKeySpec:Protected,IDisposable
{
readonly EncryptionMethods _method;
private byte[] _secretKey;
public SecretKeySpec(byte[] secretKey, EncryptionMethods encryptionMethod)
{
_secretKey = Protect(secretKey);
_method = encryptionMethod;
}
public EncryptionMethods Method => _method;
public byte[] SecretKey => Unprotect( _secretKey);
public void Dispose()
{
if (_secretKey == null)
return;
//overwrite array memory
for (int i = 0; i < _secretKey.Length; i++)
{
_secretKey[i] = 0;
}
//set-null
_secretKey = null;
}
~SecretKeySpec()
{
Dispose();
}
}
internal class Mac : Protected,IDisposable
{
byte[] rawHmac;
HMAC mac;
public Mac(SecretKeySpec key, string data)
{
switch (key.Method)
{
case EncryptionMethods.HMACMD5:
mac = new HMACMD5(key.SecretKey);
break;
case EncryptionMethods.HMACSHA512:
mac = new HMACSHA512(key.SecretKey);
break;
case EncryptionMethods.HMACSHA384:
mac = new HMACSHA384(key.SecretKey);
break;
case EncryptionMethods.HMACSHA256:
mac = new HMACSHA256(key.SecretKey);
break;
case EncryptionMethods.HMACSHA1:
mac = new HMACSHA1(key.SecretKey);
break;
default:
throw new NotSupportedException("not supported HMAC");
}
rawHmac = Protect( mac.ComputeHash(Cardinity.ENCODING.GetBytes(data)));
}
public string AsBase64()
{
return System.Convert.ToBase64String(Unprotect(rawHmac));
}
public void Dispose()
{
if (rawHmac != null)
{
//overwrite memory address
for (int i = 0; i < rawHmac.Length; i++)
{
rawHmac[i] = 0;
}
//release memory now
rawHmac = null;
}
mac?.Dispose();
mac = null;
}
~Mac()
{
Dispose();
}
}
}
如果我的假設在這里錯誤,請隨時糾正我,但是讓我解釋為什么我要問。
取自MSDN,一個SecureString
:
表示應保密的文本。 文本在使用時被加密以保護隱私,並在不再需要時從計算機內存中刪除。
我得到這個,它使完整意義上存儲的密碼或其他私人信息SecureString
在System.String
,因為您可以控制如何以及何時它實際上是存儲在內存中,因為System.String
:
既是不可變的,並且在不再需要時不能以編程方式安排進行垃圾回收; 也就是說,實例在創建后是只讀的,無法預測何時將實例從計算機內存中刪除。 因此,如果String對象包含敏感信息,例如密碼,信用卡號或個人數據,則使用該信息后可能會泄露該信息,因為您的應用程序無法從計算機內存中刪除數據。
但是,對於GUI應用程序(例如ssh客戶端),必須從System.String
構建SecureString
。 所有文本控件都使用字符串作為其基礎數據類型。
因此,這意味着即使用戶使用密碼掩碼,每次用戶按下一個鍵時,舊的字符串都會被丟棄,而新的字符串將被構建以表示文本框中的值是什么。 而且我們無法控制何時或是否從內存中丟棄這些值中的任何一個。
現在該登錄服務器了。 你猜怎么了? 您需要在連接上傳遞字符串以進行身份驗證。 因此,讓我們將SecureString
轉換為System.String
...。現在堆上有一個字符串,無法強制其通過垃圾回收(或將0寫入其緩沖區)。
我的觀點是:無論您做什么,都可以將SecureString
轉換為System.String
,這意味着它至少會在某個時刻存在於堆中(不保證任何垃圾回收)。
我的意思不是:是否有某種方法可以繞過向ssh連接發送字符串,或者可以避免通過控件存儲字符串(創建自定義控件)。 對於這個問題,您可以將“ ssh連接”替換為“登錄表格”,“注冊表格”,“付款表格”,“您要喂養的食物,而不是您的孩子的食物”,等等。
SecureString
實際上變得可行呢?System.String
對象的使用是否值得花費額外的開發時間?SecureString
的全部目的是簡單地減少System.String
在堆上的時間(減少其移至物理交換文件的風險)嗎?SecureString
阻止他進入反正數據呢?抱歉,如果我將問題放在太深的地方,好奇心只會使我變得更好。 隨時回答我的任何或所有問題(或告訴我我的假設完全錯誤)。 :)
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.