2013-01-12 52 views
18

在.NET中,我們有SecureString類,在您嘗試使用它之前,這一切都很好,例如對於字符串進行散列,您需要明文。我已經在這裏寫了一個函數來散列一個SecureString,給定一個散列函數,它需要一個字節數組並輸出一個字節數組。在.NET中散列安全字符串

private static byte[] HashSecureString(SecureString ss, Func<byte[], byte[]> hash) 
{ 
    // Convert the SecureString to a BSTR 
    IntPtr bstr = Marshal.SecureStringToBSTR(ss); 

    // BSTR contains the length of the string in bytes in an 
    // Int32 stored in the 4 bytes prior to the BSTR pointer 
    int length = Marshal.ReadInt32(bstr, -4); 

    // Allocate a byte array to copy the string into 
    byte[] bytes = new byte[length]; 

    // Copy the BSTR to the byte array 
    Marshal.Copy(bstr, bytes, 0, length); 

    // Immediately destroy the BSTR as we don't need it any more 
    Marshal.ZeroFreeBSTR(bstr); 

    // Hash the byte array 
    byte[] hashed = hash(bytes); 

    // Destroy the plaintext copy in the byte array 
    for (int i = 0; i < length; i++) { bytes[i] = 0; } 

    // Return the hash 
    return hashed; 
} 

我相信這將正確地散列字符串,並通過函數返回時,假設所提供的哈希函數性能良好,並且不作任何副本的時間將正確擦洗明文的任何副本從內存它不會擦洗自己的輸入。我錯過了什麼嗎?

+1

。注意,SecureString的可能是矯枉過正。如果攻擊者可以讀取你的記憶,你就會100%失去。 – usr

+1

@usr SecureString使用受保護的內存,因爲只有調用進程可以解密內存位置。如果您希望在應用程序崩潰時創建一個小型轉儲並將其發送給開發人員,此功能特別有用:除了您的密碼外,他們還可以獲得整個上下文,堆棧跟蹤等。 –

+0

@ M.Stramm是的,對於「冷啓動」而不是運行系統(這是攻擊面的99%)。可以讀取內存的攻擊者通常可以讀取按鍵和數據等。有有效的用例。我給你這個。 – usr

回答

13

我錯過了什麼嗎?

是的,你有一個相當基本的那個。當垃圾收集器壓縮堆時,無法清理留下的數組副本。 Marshal.SecureStringToBSTR(ss)可以,因爲BSTR分配在非託管內存中,因此將有一個不會更改的可靠指針。換句話說,沒有問題可以清理那個。

然而,您的byte[] bytes數組包含該字符串的副本,而則是在GC堆上分配的。您可能會使用hashed []數組引發垃圾回收。很容易避免,但是當然你很難控制你的進程中的其他線程分配內存和引發一個集合。或者就此而言,當您的代碼開始運行時,背景GC已經在進行中。

SecureString的要點是從來沒有在垃圾收集的內存中有明確的字符串副本。將其複製到託管數組違反了該保證。如果你想使這個代碼安全,那麼你將不得不編寫一個採用IntPtr的hash()方法,並且只讀取該指針。

請注意,如果您的散列需要匹配在另一臺機器上計算出的散列,那麼您不能忽略機器用於將字符串轉換爲字節的編碼。

+0

嗯。實現一個初始化的有狀態散列回調是否更有意義,然後每次輸入一個字節,這樣散列回調本身只需要處理託管類型? –

+0

棘手;問題是散列函數像Rfc2898DeriveBytes.GetBytes,這是我正在計劃使用的,只能使用託管類型。你會有任何建議,只使用非託管內存的替代方案嗎? –

+0

關於編碼 - BSTR是UTF-16,所以AFAIK在機器間保持一致。 –

3

作爲對Hans的回答的補充,這裏給出了一個如何實現hasher的建議。 Hans建議將指向非託管字符串的指針傳遞給哈希函數,但這意味着客戶端代碼(=哈希函數)需要處理非託管內存。這並不理想。

在另一方面,你可以通過下面的接口的實例代替回調:

interface Hasher { 
    void Reinitialize(); 
    void AddByte(byte b); 
    byte[] Result { get; } 
} 

這樣散列器(雖然它變得稍微複雜一點的),可以在管理土地全部實現無泄漏安全信息。然後你會HashSecureString如下所示:

private static byte[] HashSecureString(SecureString ss, Hasher hasher) { 
    IntPtr bstr = Marshal.SecureStringToBSTR(ss); 
    try { 
     int length = Marshal.ReadInt32(bstr, -4); 

     hasher.Reinitialize(); 

     for (int i = 0; i < length; i++) 
      hasher.AddByte(Marshal.ReadByte(bstr, i)); 

     return hasher.Result; 
    } 
    finally { 
     Marshal.ZeroFreeBSTR(bstr); 
    } 
} 

注意finally塊,以確保非託管內存歸零,無論什麼把戲散列器實例確實。

這裏有一個簡單的(不是很有用)Hasher實施來說明接口:

sealed class SingleByteXor : Hasher { 
    private readonly byte[] data = new byte[1]; 

    public void Reinitialize() { 
     data[0] = 0; 
    } 

    public void AddByte(byte b) { 
     data[0] ^= b; 
    } 

    public byte[] Result { 
     get { return data; } 
    } 
} 
+1

此處進行Nitpicking:如果GC恰好在恰當的時刻移動字節[],則可能會泄漏第一個未加密的密鑰字節。無論如何,因爲使用SecureString首先是一個錯誤。人們不能期望這樣的系統太多。 – usr

+0

@us對我而言,這是真的,並且令人非常不滿意。不幸的是,除非整個哈希計算是在非託管內存中完成的,否則我沒有看到解決這個問題的方法,這也是漢斯的建議代碼出現的根本問題。也許這實際上是唯一安全的方法。 –

+1

@usr/@KonradRudolph - 這可以通過'GCHandle'緩解嗎?例如 'byte [] bytes; GCHandle handle = GCHandle.Alloc(bytes = new byte [1],GCHandleType.Pinned); handle.Free();' – jimbobmcgee

2

作爲進一步的補充,你能不能換邏輯@KonradRudolph和@HansPassant供給到定製Stream實施?

這將允許您使用HashAlgorithm.ComputeHash(Stream)方法,該方法可以保持接口的管理(儘管您應該及時處理流)。

當然,你在的HashAlgorithm實施擺佈多少數據存儲在一個時間結束了(但是,當然,這就是參考源是!)

只是一個想法...

public class SecureStringStream : Stream 
{ 
    public override bool CanRead { get { return true; } } 
    public override bool CanWrite { get { return false; } } 
    public override bool CanSeek { get { return false; } } 

    public override long Position 
    { 
     get { return _pos; } 
     set { throw new NotSupportedException(); } 
    } 

    public override void Flush() { throw new NotSupportedException(); } 
    public override long Seek(long offset, SeekOrigin origin) { throw new NotSupportedException(); } 
    public override void SetLength(long value) { throw new NotSupportedException(); } 
    public override void Write(byte[] buffer, int offset, int count) { throw new NotSupportedException(); } 

    private readonly IntPtr _bstr = IntPtr.Zero; 
    private readonly int _length; 
    private int _pos; 

    public SecureStringStream(SecureString str) 
    { 
     if (str == null) throw new ArgumentNullException("str"); 
     _bstr = Marshal.SecureStringToBSTR(str); 

     try 
     { 
      _length = Marshal.ReadInt32(_bstr, -4); 
      _pos = 0; 
     } 
     catch 
     { 
      if (_bstr != IntPtr.Zero) Marshal.ZeroFreeBSTR(_bstr); 
      throw; 
     } 
    } 

    public override long Length { get { return _length; } } 

    public override int Read(byte[] buffer, int offset, int count) 
    { 
     if (buffer == null) throw new ArgumentNullException("buffer"); 
     if (offset < 0) throw new ArgumentOutOfRangeException("offset"); 
     if (count < 0) throw new ArgumentOutOfRangeException("count"); 
     if (offset + count > buffer.Length) throw new ArgumentException("offset + count > buffer"); 

     if (count > 0 && _pos++ < _length) 
     { 
      buffer[offset] = Marshal.ReadByte(_bstr, _pos++); 
      return 1; 
     } 
     else return 0; 
    } 

    protected override void Dispose(bool disposing) 
    { 
     try { if (_bstr != IntPtr.Zero) Marshal.ZeroFreeBSTR(_bstr); } 
     finally { base.Dispose(disposing); } 
    } 
} 

void RunMe() 
{ 
    using (SecureString s = new SecureString()) 
    { 
     foreach (char c in "jimbobmcgee") s.AppendChar(c); 
     s.MakeReadOnly(); 

     using (SecureStringStream ss = new SecureStringStream(s)) 
     using (HashAlgorithm h = MD5.Create()) 
     { 
      Console.WriteLine(Convert.ToBase64String(h.ComputeHash(ss))); 
     } 
    } 
} 
+1

這與'HansPassant'調出相同的問題,你仍然得到緩衝區中字符串的副本。你可以讓'Read'一次只返回一個字節,它的權限是'Read'的一個內在因素,但是你不能保證'ComputeHash(Stream)'不會保存傳入的緩衝區進入托管內存。 –

+0

@ScottChamberlain - 我喜歡'Read'這個概念,一次只返回1個字節(並修改了這個例子來表明這一點)。我留下了我原來的評論*「在HashAlgorithm實施的擺佈」*,這已經與您的*「不保證......不僅僅是存儲」*--)相一致 – jimbobmcgee

+1

不是那麼重要,但是您有一個小故障你的新例子,你不處理'count == 0',你可以讓你的if語句變成'if(count> 0 && _pos ++ <_length)' –

3

總是有使用非託管CryptoApiCNG功能的可能性。 請記住,SecureString是設計用於管理內存管理的完全控制的非託管用戶。

如果你想堅持到C#,你應該釘住臨時數組阻止GC四處移動它,你得到一個機會,擦洗前:

private static byte[] HashSecureString(SecureString input, Func<byte[], byte[]> hash) 
{ 
    var bstr = Marshal.SecureStringToBSTR(input); 
    var length = Marshal.ReadInt32(bstr, -4); 
    var bytes = new byte[length]; 

    var bytesPin = GCHandle.Alloc(bytes, GCHandleType.Pinned); 
    try { 
     Marshal.Copy(bstr, bytes, 0, length); 
     Marshal.ZeroFreeBSTR(bstr); 

     return hash(bytes); 
    } finally { 
     for (var i = 0; i < bytes.Length; i++) { 
      bytes[i] = 0; 
     } 

     bytesPin.Free(); 
    } 
} 
+1

現在我理解你的評論,並且據我所見,這個解決方案應該確實可行。唯一需要注意的是,客戶端可能會試圖實現一個'hash'函數來重用'byte []'參數作爲返回值(計算就地散列)。這顯然會失敗。但我認爲這不是一個真正的問題。 –

+1

一個更真實的問題是散列函數的實現者不應該複製那個'byte []'參數。可悲的是Crypto命名空間中的大多數哈希似乎都是出於性能原因。哦,另一個問題,另一個問題:) –