繁体   English   中英

SecureString 在 C# 应用程序中实用吗?

[英]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在堆上的时间(降低其移动到物理交换文件的风险)?
  • 如果攻击者已经拥有进行堆检查的手段,那么他很可能 (A) 已经拥有读取击键的手段,或者 (B) 已经物理上拥有机器......所以使用SecureString阻止他获得数据呢?
  • 这只是“通过默默无闻的安全”吗?

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

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

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

  • 密码意外出现在日志文件中。
  • 在某处显示密码 - 一旦 GUI 确实显示了正在运行的应用程序的命令行,并且命令行由密码组成。 哎呀
  • 使用内存分析器与您的同事一起分析软件。 同事在记忆中看到您的密码。 听起来不真实? 一点也不。
  • 我曾经使用过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 需要它。

通常的问题是:

  1. API 是通用的。 它不知道有敏感数据。
  2. 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) ) 中EncryptSecureStringCore(SecureString secureString)

作为一些非常相关的东西: Mono SecureString 根本不加密 该实现已被注释掉,因为..等待它.. “它以某种方式导致 nunit 测试损坏” ,这带来了我的最后一点:

并非所有地方都支持SecureString 如果平台/架构不支持SecureString ,您将收到异常。 文档中提供了支持的平台列表。

您的假设中很少有问题。

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

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

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

因此,例如,要使用它来通过 Web 进行身份验证,您必须使用同样安全的适当类。

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

  • WPF 的 PasswordBox 控件在内部将密码保存为 SecureString。
  • System.Diagnostics.ProcessInfo 的 Password 属性是 SecureString。
  • X509Certificate2 的构造函数采用 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特定优势:确定性擦除。 这个事实有两个问题:

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

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

以下文字复制自 HP Fortify 静态代码分析器

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

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

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

我想说明这一点:

如果攻击者已经拥有进行堆检查的方法,那么他们很可能 (A) 已经拥有读取击键的方法,或者 (B) 已经物理上拥有机器......那么使用SecureString阻止他们进入数据呢?

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

HeartBleed 内存泄漏

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

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

但是,可以对 GUI 进行编程以使用SecureString ,在这种情况下,减少内存中的密码可用性窗口是值得的。 例如,WPF 中的PasswordBox.SecurePasswordSecureString类型。

前段时间我不得不针对 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

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

我得到这个,它使完整意义上存储的密码或其他私人信息SecureStringSystem.String ,因为您可以控制如何以及何时它实际上是存储在内存中,因为System.String

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

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

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

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

我的观点是:无论您做什么,可以将SecureString转换为System.String ,这意味着它至少会在某个时刻存在于堆中(不保证任何垃圾回收)。

我的意思不是:是否有某种方法可以绕过向ssh连接发送字符串,或者可以避免通过控件存储字符串(创建自定义控件)。 对于这个问题,您可以将“ ssh连接”替换为“登录表格”,“注册表格”,“付款表格”,“您要喂养的食物,而不是您的孩子的食物”,等等。

  • 那么,在什么时候使用SecureString实际上变得可行呢?
  • 完全根除System.String对象的使用是否值得花费额外的开发时间?
  • SecureString的全部目的是简单地减少System.String在堆上的时间(减少其移至物理交换文件的风险)吗?
  • 如果攻击者已经具有检查堆的方法,那么他很可能要么(A)已经具有读取击键的方法,要么(B)已经物理拥有了机器……因此使用SecureString阻止他进入反正数据呢?
  • 这仅仅是“默默无闻的安全”吗?

抱歉,如果我将问题放在太深的地方,好奇心只会使我变得更好。 随时回答我的任何或所有问题(或告诉我我的假设完全错误)。 :)

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM