SecureString 在 C# 应用程序中是不是实用?

Posted

技术标签:

【中文标题】SecureString 在 C# 应用程序中是不是实用?【英文标题】:Is SecureString ever practical in a C# application?SecureString 在 C# 应用程序中是否实用? 【发布时间】:2014-11-29 05:16:32 【问题描述】:

如果我在这里的假设有误,请随时纠正我,但让我解释一下我问的原因。

取自 MSDN,SecureString

表示应保密的文本。文本在使用时被加密以保护隐私,并在不再需要时从计算机内存中删除。

我明白了,将密码或其他私人信息存储在 SecureString 中而不是 System.String 中是完全有意义的,因为您可以控制它实际存储在内存中的方式和时间,因为 System.String

是不可变的,并且在不再需要时,不能以编程方式安排垃圾收集;也就是说,实例在创建后是只读的,无法预测实例何时会从计算机内存中删除。因此,如果 String 对象包含密码、信用卡号或个人数据等敏感信息,则存在使用该信息后泄露的风险,因为您的应用程序无法从计算机内存中删除数据。

但是,对于 GUI 应用程序(例如,ssh 客户端),SecureString 必须从 System.String 构建。所有文本控件都使用字符串作为其基础数据类型

因此,这意味着每次用户按下一个键时,旧字符串都会被丢弃,并且会构建一个新字符串来表示文本框中的值,即使使用密码掩码也是如此。 而且我们无法控制这些值何时或是否从内存中丢弃

现在是时候登录服务器了。你猜怎么了? 您需要通过连接传递一个字符串以进行身份​​验证。因此,让我们将 SecureString 转换为 System.String.... 现在我们在堆上有一个字符串,无法强制它通过垃圾收集(或将 0 写入其缓冲区)。

我的意思是:无论你做什么,在某个地方,SecureString转换为System.String,这意味着它将至少在某个时候存在于堆上(不保证垃圾回收)。

我的观点不是:是否有办法规避向 ssh 连接发送字符串,或规避让控件存储字符串(制作自定义控件)。对于这个问题,您可以将“ssh connection”替换为“login form”、“registration form”、“payment form”、“foods-you-would-feed-your-puppy-but-not-your-children form”,等等

那么,在什么时候使用SecureString 实际上变成 实用吗? 是否值得花费额外的开发时间来彻底根除 使用System.String 对象? SecureString 的全部意义在于简单地减少System.String 在堆上的时间(降低其移动到物理交换文件的风险)? 如果攻击者已经拥有堆检查的手段,那么他很可能 (A) 已经拥有读取击键的手段,或者 (B) 已经实际拥有机器...那么使用SecureString 会阻止他获取数据吗? 这只是“通过默默无闻的安全”吗?

对不起,如果我把问题说得太粗了,好奇心就占了上风。随意回答我的任何或所有问题(或告诉我我的假设完全错误)。 :)

【问题讨论】:

请记住,SecureString 并不是真正的安全字符串。这只是减少某人可以检查您的内存并成功获取敏感数据的时间窗口的一种方法。这不是防弹的,也不是故意的。但是你提出的观点是非常有效的。相关:***.com/questions/14449579/… @TheodorosChatzigiannakis,是的,这就是我的想法。我今天整天都在绞尽脑汁想找到一种安全的方法来存储应用程序生命周期内的密码,这让我想知道,这值得吗? 可能不值得。但是,再一次,您可能正在防御极端情况。极端并不是说某人不太可能获得对您计算机的这种访问权限,而是从某种意义上说,如果某人确实获得了这种访问权限,则计算机被认为(出于所有意图和目的)受到了损害,而我没有不认为有任何语言或任何技术可以用来完全防御这种情况。 它可以防止密码在 内可见,但在所有人不再认为可能存在问题之后很久。在废弃的硬盘上,存储在页面文件中。擦洗内存以降低这种可能性很重要,不能使用 System.String 来做到这一点,因为它是不可变的。它需要当今很少有程序可以访问的基础架构,与本机代码互操作,因此 Marshal.SecureStringXxx() 方法有用的情况越来越少。或者是一种体面的方式来提示用户:) SecureString 是一种深入的安全技术。在大多数情况下,开发成本和安全收益之间的权衡是不利的。很少有好的用例。 【参考方案1】:

Microsoft 不建议将 SecureString 用于较新的代码。

来自SecureString Class的文档:

重要

我们不建议您将 SecureString 类用于新的 发展。欲了解更多信息,请参阅SecureString shouldn't be used

推荐:

不要将SecureString 用于新代码。将代码移植到 .NET Core 时, 考虑到数组的内容在内存中没有加密。

处理凭据的一般方法是避免使用它们并 而是依靠其他方式进行身份验证,例如证书或 Windows 身份验证。 在 GitHub 上。

【讨论】:

【参考方案2】:

前段时间,我不得不针对 java 信用卡支付网关创建一个 c# 接口,并且需要一个兼容的安全通信密钥加密。由于 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();
        
    

【讨论】:

【参考方案3】:

SecureString 实际上有非常实际的用途。

你知道我见过多少次这样的场景吗? (答案是:很多!):

密码意外出现在日志文件中。 密码显示在某处 - 一旦 GUI 确实显示了正在运行的应用程序的命令行,并且命令行包含密码。 糟糕。 使用内存分析器与您的同事一起分析软件。同事在内存中看到您的密码。听起来不真实?一点也不。 我曾经使用过RedGate 软件,它可以在异常情况下捕获局部变量的“值”,非常有用。不过,我可以想象它会意外记录“字符串密码”。 包含字符串密码的故障转储。

您知道如何避免所有这些问题吗? SecureString。它通常可以确保你不会犯这样的愚蠢错误。它是如何避免的?通过确保密码在非托管内存中加密,并且只有在您对自己在做什么有 90% 的把握时才能访问真正的值。

从某种意义上说,SecureString 很容易工作:

1) 一切都是加密的

2) 用户拨打AppendChar

3) 解密 UNMANAGED MEMORY 中的所有内容并添加角色

4) 再次加密 UNMANAGED MEMORY 中的所有内容。

如果用户可以访问您的计算机怎么办?病毒是否能够访问所有SecureStrings?是的。你需要做的就是在内存被解密的时候把自己钩入RtlEncryptMemory,你会得到未加密的内存地址的位置,并读出它。瞧!事实上,您可以制作一个病毒,它会不断扫描SecureString 的使用情况并用它记录所有活动。我并不是说这将是一件容易的事,但它是可以完成的。如您所见,一旦您的系统中有用户/病毒,SecureString 的“强大功能”就完全消失了。

您的帖子中有几点。当然,如果您使用一些在内部保存“字符串密码”的 UI 控件,则使用实际的SecureString 并没有那么有用。尽管如此,它仍然可以防止我上面列出的一些愚蠢行为。

此外,正如其他人所指出的,WPF 支持 PasswordBox,它通过其 SecurePassword property. 在内部使用 SecureString

底线是;如果您有敏感数据(密码、信用卡等),请使用SecureString。这就是 C# 框架所遵循的。例如,NetworkCredential 类将密码存储为SecureString。如果您查看this,您可以在SecureString 的.NET 框架中看到大约80 多种不同的用法。

在很多情况下,您必须将 SecureString 转换为字符串,因为某些 API 需要它。

通常的问题是:

    API 是通用的。它不知道有敏感数据。 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),它为您提供SecureStringstring 实例,该实例使用服务器的公钥加密。然后只有服务器可以解密它。问题已解决:垃圾收集永远不会看到“原始”字符串,因为您永远不会在托管内存中公开它。这正是PSRemotingCryptoHelper (EncryptSecureStringCore(SecureString secureString)) 正在做的事情。

并且几乎相关:Mono SecureString 根本不加密。该实现已被注释掉,因为..wait for it.."It somehow causes nunit test breakage",这带来了我的最后一点:

SecureString 在任何地方都不受支持。如果平台/架构不支持SecureString,你会得到一个异常。文档中有一个受支持的平台列表。

【讨论】:

我在每次执行的源代码审计中都标记了这一点。还应该重复的是,如果您使用SecureString,则需要在整个堆栈中一直使用它。 我实现了 .ToEncryptedString() 扩展,但使用了证书。你介意看看,如果我做错了,请告诉我。我希望它的安全 engouhg gist.github.com/sturlath/99cfbfff26e0ebd12ea73316d0843330 new documentation from Microsoft stating that SecureString should not be used for new development 呢? @julealgon - 这是一个很好的提示,但 Micsosoft 没有非常全面地解释它应该如何实现。我找到了第二个链接,this one,讨论 SecureString 的安全性。结论是它不是很安全,但比普通字符串更好。现在我很困惑。我自己的结论是,在您的项目中使用 SecureString 来查看可保护的敏感凭证在哪里,并在最后一步用您自己的兼容强加密类替换它,使用 AES 等。 @Matt 详细地说,是的,SecureString 不安全,但是类的名称给出了一个不那么微妙的暗示,即它安全的。 MS 建议人们不要使用它,因为人们将它混淆为安全实施。但是,正如本答案中所述,SecureString 确实可以保护您免受愚蠢错误的侵害。一般来说,安全性应该分层处理,不应该完全信任任何一个层来保证安全性。 SecureString 可能是朝着正确方向迈出的一小步,但正如答案中所述,总比没有好。【参考方案4】:

我想谈谈这一点:

如果攻击者已经拥有堆检查的手段,那么他们很可能是 (A) 已经拥有读取击键的手段,或者 (B) 已经物理上拥有机器...那么使用SecureString 会阻止他们获取数据吗?

攻击者可能无法完全访问计算机和应用程序,但可以访问进程内存的某些部分。当特殊构造的输入可能导致应用程序暴露或覆盖内存的某些部分时,通常是由缓冲区溢出等错误引起的。

HeartBleed 内存泄漏

以 Heartbleed 为例。特殊构造的请求会导致代码将进程内存的随机部分暴露给攻击者。攻击者可以从内存中提取 SSL 证书,但他唯一需要的只是使用格式错误的请求。

在托管代码的世界中,缓冲区溢出成为问题的频率要低得多。在 WinForms 的情况下,数据已经以不安全的方式存储,您对此无能为力。这使得SecureString 的保护几乎毫无用处。

然而,GUI可以被编程为使用SecureString,在这种情况下减少内存中密码可用性的窗口是值得的。例如,来自 WPF 的 PasswordBox.SecurePassword 的类型为 SecureString

【讨论】:

嗯,知道肯定很有趣。老实说,我实际上并不担心什至从我的记忆中获取 System.String 值所需的攻击类型,我只是出于纯粹的好奇而询问。不过,谢谢你的信息! :)【参考方案5】:

以下文本是从 HP Fortify 静态代码分析器复制而来的

摘要: PassGenerator.cs 中的方法 PassString() 以不安全的方式(即字符串)存储敏感数据,从而可以提取 通过检查堆来获取数据。

解释: 存储在内存中的敏感数据(如密码、社会保险号、信用卡号等)如果被泄露,可能会被泄露。 存储在托管的 String 对象中。字符串对象没有固定,因此垃圾收集器可以随意重定位这些对象并 在内存中留下几份副本。这些对象默认是不加密的,所以任何可以读取进程内存的人都会被 能够看到内容。此外,如果进程的内存被换出到磁盘,则字符串的未加密内容 将被写入交换文件。最后,由于 String 对象是不可变的,从内存中删除 String 的值只能 由 CLR 垃圾收集器完成。除非 CLR 内存不足,否则不需要运行垃圾收集器,因此有 不保证何时进行垃圾收集。在应用程序崩溃的情况下,内存转储 应用程序可能会泄露敏感数据。

建议: 不要将敏感数据存储在字符串等对象中,而是将它们存储在 SecureString 对象中。每个对象将其内容存储在一个 内存中的加密格式。

【讨论】:

【参考方案6】:

您的假设中很少有问题。

首先,SecureString 类没有 String 构造函数。为了创建一个,您分配一个对象,然后附加字符。

在 GUI 或控制台的情况下,您可以非常轻松地将每个按下的键传递给安全字符串。

该类的设计方式使您无法错误地访问存储的值。这意味着您无法直接从中获取string 作为密码。

因此,要使用它,例如通过网络进行身份验证,您将必须使用适当的安全类。

在 .NET 框架中,您有一些可以使用 SecureString 的类

WPF 的 PasswordBox 控件在内部将密码保存为 SecureString。 System.Diagnostics.ProcessInfo 的 Password 属性是 SecureString。 X509Certificate2 的构造函数采用 SecureString 作为密码。

(more)

总之,SecureString 类可能很有用,但需要开发人员更多的关注。

所有这一切,连同例子,在MSDN的SecureString文档中都有很好的描述

【讨论】:

我不太明白你回答的倒数第三段。 如何通过网络传输SecureString 而不将其序列化为string?我认为问题的 OP 的观点是,有一段时间你想要真正使用安全地保存在SecureString 中的值,这意味着你必须把它从那里拿出来,它不再是安全的。在所有框架类库中,几乎没有方法可以直接接受SecureString 作为输入(据我所知),那么首先在SecureString 中保存一个值有什么意义呢? @DamianLeszczyński-Vash,我知道 SecureString 没有字符串构造函数,但我的意思是您要么(A)创建一个 SecureString 并将字符串中的每个字符附加到它,要么( B)在某些时候需要使用存储在 SecureString 中的值作为字符串,以便将其传递给某些 API。话虽如此,您确实提出了一些好的观点,我可以看到它在一些非常具体的情况下是如何有用的。 @stakx 您可以通过网络发送它,方法是获取char[] 并通过套接字发送每个char - 在此过程中不会创建垃圾收集对象。 Char[] 是可收集的并且是可变的,所以它可以被覆盖。 当然,忘记覆盖该数组也很容易。无论哪种方式,您将字符传递给的任何 API 也可以进行复制。【参考方案7】:

SecureString 在以下情况下很有用:

您可以逐个字符地构建它(例如,从控制台输入)或从非托管 API 获取它

您可以通过将其传递给非托管 API (SecureStringToBSTR) 来使用它。

如果您曾经将其转换为托管字符串,那么您就违背了它的目的。

更新回应评论

...或者你提到的 BSTR,这似乎不再安全

在转换为 BSTR 后,使用 BSTR 的非托管组件可以将内存归零。非托管内存更安全,因为它可以通过这种方式重置。

但是,.NET Framework 中支持 SecureString 的 API 非常少,因此您可以说它在今天的价值非常有限。

我会看到的主要用例是在要求用户输入高度敏感的代码或密码的客户端应用程序中。可以逐个字符地使用用户输入来构建 SecureString,然后将其传递给非托管 API,该 API 在使用后将接收到的 BSTR 清零。任何后续的内存转储都不会包含敏感字符串。

在服务器应用程序中,很难看出它的用处。

更新 2

接受 SecureString 的 .NET API 的一个示例是 this constructor for the X509Certificate class。如果您使用 ILSpy 或类似工具进行探索,您会看到 SecureString 在内部转换为非托管缓冲区 (Marshal.SecureStringToGlobalAllocUnicode),然后在使用 (Marshal.ZeroFreeGlobalAllocUnicode) 完成时将其归零。

【讨论】:

+1,但你不会总是以“失败的目的”告终吗?鉴于框架类库中几乎没有任何方法接受SecureString 作为输入,它们的用途是什么?您总是必须迟早转换回string(或BSTR,就像您提到的那样,这似乎不再安全)。那么,为什么还要打扰SecureString 呢?【参考方案8】:

正如您正确识别的那样,SecureString 提供了一个优于 string 的特定优势:确定性擦除。这个事实有两个问题:

    正如其他人所提到的以及您自己所注意到的,这本身是不够的。您必须确保过程的每一步(包括检索输入、构造字符串、使用、删除、传输等)都不会违背使用SecureString 的目的。这意味着您必须小心,切勿创建由 GC 管理的不可变 string 或任何其他将存储敏感信息的缓冲区(否则您还必须跟踪 )。在实践中,这并不总是很容易实现,因为许多 API 只提供了一种使用 string 的方法,而不是 SecureString。即使你做对了一切...... SecureString 可以防止非常特定类型的攻击(对于其中一些攻击,它甚至不那么可靠)。例如,SecureString 确实允许您缩小攻击者可以转储进程内存并成功提取敏感信息的时间窗口(再次,正如您正确指出的那样),但希望窗口太小攻击者拍摄你的记忆快照根本不被视为安全。

那么,你应该什么时候使用它呢?只有当您使用的东西可以让您使用SecureString 来满足您的所有需求时,您仍然应该注意这仅在特定情况下是安全的。

【讨论】:

您假设攻击者能够在任何特定时间对进程的内存进行快照。情况不一定如此。考虑一个崩溃转储。如果在应用程序处理完敏感数据后发生崩溃,则故障转储不应包含敏感数据。

以上是关于SecureString 在 C# 应用程序中是不是实用?的主要内容,如果未能解决你的问题,请参考以下文章

如何将 SecureString 从一个进程传递到另一个进程?

字符串在 C# 中是如何工作的? [关闭]

MySQL SecureString 作为连接字符串

在 .NET 中散列 SecureString

在C#中如何判断一个对象是不是是某个类型的实例

如何将 SecureString 转换为 System.String?