SecureString在C#应用程序中是否可行?


224

如果我的假设在这里错误,请随时纠正我,但是让我解释为什么我要问。

取自MSDN,是SecureString

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

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

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

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

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

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

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

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

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

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


25
请记住,这SecureString并不是真正的安全字符串。这只是减少时间窗口的一种方法,人们可以在该时间窗口中检查您的内存并成功获取敏感数据。这不是防弹,也不是故意的。但是您提出的观点非常有效。相关:stackoverflow.com/questions/14449579/...
塞奥佐罗斯Chatzigiannakis

1
@TheodorosChatzigiannakis,是的,这就是我所想的。我今天整整花了整整一天的时间思考如何找到一种安全的方法来存储应用程序寿命的密码,这让我感到奇怪,这值得吗?
Steven Jeffries 2014年

1
可能不值得。但话又说回来,您可能正在防御极端情况。极端不是从某种意义上说某人不太可能获得对您计算机的访问权限,而是从某种意义上说,如果某人确实获得了这种访问权限,则该计算机(就所有意图和目的而言)都将受到损害,而我不会认为您可以使用任何语言或任何技术来完全抵御这种攻击。
Theodoros Chatzigiannakis 2014年

8
在所有人停止思考可能存在问题的很长时间之后,它可以防止密码可见多年。在废弃的硬盘驱动器上,存储在页面文件中。清理内存以使发生这种情况的几率很低很重要,因为System.String是不可变的,因此无法使用System.String做到这一点。现在需要很少的程序可以访问的基础结构,并且与本机代码互操作,因此Marshal.SecureStringXxx()方法非常有用,这种情况越来越少。或者提示用户的一种体面方式:)
Hans Passant 2014年

1
SecureString是一种深度安全技术。在大多数情况下,开发成本与安全性收益之间的权衡是不利的。很少有很好的用例。
usr 2014年

Answers:


237

实际上有非常实用的用法SecureString

您知道我见过多少次这种情况吗?(答案是:很多!):

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

您知道如何避免所有这些问题吗?SecureString。通常可以确保您不会犯此类愚蠢的错误。如何避免呢?通过确保在非托管内存中对密码进行加密,只有在您确定自己在做什么的90%时,才可以访问真实值。

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

1)一切都加密

2)用户通话 AppendChar

3)解密“未管理的内存”中的所有内容并添加字符

4)在“未管理的内存”中再次加密所有内容。

如果用户可以访问您的计算机怎么办?病毒是否可以访问所有SecureStrings?是。您需要做RtlEncryptMemory的就是在解密内存时陷入困境,您将获得未加密内存地址的位置,并将其读出。瞧!实际上,您可能制作了一种病毒,该病毒将不断对其进行扫描SecureString并记录所有活动。我并不是说这将是一件容易的事,但可以做到。如您所见,SecureString一旦系统中存在用户/病毒,“功能”将完全消失。

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

另外,正如其他人指出的那样,WPF支持SecureString通过其SecurePassword属性在内部使用的PasswordBox

底线是; 如果您有敏感数据(密码,信用卡等),请使用SecureString。这就是C#Framework所遵循的。例如,NetworkCredentialclass将密码存储为SecureString。如果您查看此内容,则可以在.NET框架中看到超过80种不同的用法SecureString

在许多情况下,您必须转换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)为您提供了使用服务器的公共密钥加密的string实例SecureString。然后只有服务器才能对其解密。解决的问题:垃圾回收将永远不会看到“原始”字符串,因为您永远不会在托管内存中公开它。这正是PSRemotingCryptoHelperEncryptSecureStringCore(SecureString secureString))中所做的。

并且作为一个几乎与之相关的东西:Mono SecureString根本不加密。该实现已被注释掉,因为..等待它。“它以某种方式导致nunit测试损坏”,这引出了我的最后一点:

SecureString并非在所有地方都受支持。如果平台/体系结构不支持SecureString,您将获得一个例外。文档中有受支持的平台列表。


我认为SecureString,如果有一种机制可以访问其各个字符,那将更加实用。通过具有SecureStringTempBuffer不带任何公共成员的struct类型,可以提高这种机制的效率,可以将其传递refSecureString“读取一个字符”方法[如果加密/解密以8个字符的块进行处理,则这种结构可以保存数据8个字符以及[]中第一个字符的位置SecureString
2014年

2
我在执行的每个源代码审核中都标记了这一点。还应该重复一遍,如果使用SecureString它,则需要在整个堆栈中使用它。
凯西2015年

1
我通过使用证书实现了.ToEncryptedString()扩展名。您介意看看吗,让我知道我做错了什么。I'm希望其安全engouhg gist.github.com/sturlath/99cfbfff26e0ebd12ea73316d0843330
Sturla

@Sturla — EngineSettings您的扩展方法是什么?
InteXX


15

假设中的几个问题。

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

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

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

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

在.NET Framework中,您可以使用一些类来使用SecureString

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

(更多)

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

所有这些以及示例在MSDN的SecureString文档中都有很好的描述。


10
我不太理解您回答的倒数第二段。您如何SecureString在不将其序列化为网络的情况下通过网络进行传输string?我认为问题OP的要点是,有时候您想真正使用 a中安全保存的值SecureString,这意味着您必须将其取出来,并且不再安全。在所有Framework类库中,实际上没有SecureString直接接受a 作为输入的方法(据我所知),那么SecureString首先在a 中保存值的意义是什么?
stakx-不再贡献

@DamianLeszczyński-Vash,我知道SecureString没有字符串构造函数,但我的意思是,您可以(A)创建一个SecureString并将字符串中的每个字符追加到它,或者(B)在某个时候需要使用以字符串形式存储在SecureString中的值,以便将其传递给某些API。话虽如此,您的确提出了一些要点,我可以看到在某些非常特殊的情况下它如何有用。
Steven Jeffries 2014年

1
@stakx您可以通过获取char[]char通过套接字发送每个消息来通过网络发送它-在此过程中不创建垃圾可收集对象。
user253751 2014年

Char []是可收集的并且是可变的,因此可以将其覆盖。
彼得·里奇

当然,忘记覆盖该数组也很容易。无论哪种方式,传递给char的任何API都可能会复制。
cHao 2014年

10

如果满足以下条件,则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)后将其清零。


1
+1,但您是否总不会“击败它的目的”?鉴于Framework类库中几乎没有方法接受a SecureString作为输入,它们的用途是什么?您总是必须string迟早转换回原先的状态(或BSTR像您提到的那样,这似乎不再安全)。那么,为什么还要打扰SecureString呢?
stakx-在14:10不再贡献

5

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

SecureString类的文档中:

重要

我们不建议您将该SecureString类用于新开发。有关更多信息,请参见SecureString不应使用

哪个建议:

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

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


4

正如您已正确识别出的那样,SecureStringstring确定性擦除相比,它提供了一个特定的优势。这个事实有两个问题:

  1. 正如其他人提到的和您自己注意到的那样,仅凭这还不够。您必须确保过程的每个步骤(包括输入的检索,字符串的构造,用法,删除,运输等)都在不违反使用目的的情况下发生SecureString。这意味着你必须小心不要再创建一个GC管理不变string或将存储敏感信息的任何其它缓冲区(或者你要跟踪的也)。在实践中,这并非总是容易实现的,因为许多API仅提供了一种使用方式string,而不是SecureString。即使您确实做到了一切正确...
  2. SecureString可以防御非常特殊的攻击(对于某些攻击,甚至还不够可靠)。例如,SecureString确实允许您缩小时间范围,攻击者可以在该时间范围内转储进程的内存并成功提取敏感信息(同样,如您所正确指出的那样),但希望该窗口对于攻击者而言太小而无法对您的内存进行快照根本不算安全。

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


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

4

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

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

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

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


3

我想解决这一点:

如果攻击者已经具有进行堆检查的手段,则他们很可能(A)已经具有读取击键的手段,或者(B)已经物理上拥有机器 ……因此可以使用SecureString防止他们进入的方法。反正数据?

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

HeartBleed泄漏记忆

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

在托管代码的世界中,缓冲区溢出问题很少出现。对于WinForms,数据已经以不安全的方式存储,您对此无能为力。这使得保护SecureString几乎没有用。

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


嗯,肯定很有趣。老实说,我实际上并不担心什至要从内存中获取System.String值所必需的攻击类型,我只是出于好奇。但是,感谢您提供的信息!:)
史蒂芬·杰弗里斯

2

前一段时间,我不得不针对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();
        }
    }
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.