何时需要在.NET中使用SecureString?

186

我试图理解.NET的SecureString的目的。从MSDN得知:

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

SecureString对象与String对象类似,都具有文本值。但是,SecureString对象的值是自动加密的,可以在您的应用程序将其标记为只读之前进行修改,并且可以由您的应用程序或.NET Framework垃圾收集器从计算机内存中删除。

当初始化实例或修改值时,SecureString实例的值会自动加密。您的应用程序可以通过调用MakeReadOnly方法使实例变为不可变,并防止进一步修改。

自动加密是重要的优势吗?

为什么我不能只说:

SecureString password = new SecureString("password");

取代

SecureString pass = new SecureString();
foreach (char c in "password".ToCharArray())
    pass.AppendChar(c);

我缺少哪些关于SecureString的方面?


8
11年后,微软不再推荐使用SecureString进行新开发:https://github.com/dotnet/platform-compat/blob/master/docs/DE0001.md - Matt Thomas
11个回答

108

当前框架中使用SecureString的部分如下:

主要目的是减少攻击面,而不是消除它。SecureStrings在内存中被“固定”,因此垃圾回收器不会移动它或复制它。它还确保明文不会被写入交换文件或核心转储。加密更像是混淆,无法阻止决心坚定的黑客找到用于加密和解密的对称密钥
正如其他人所说,你必须逐个字符创建SecureString的原因是因为这样做的第一个明显缺陷:你可能已经有了明文字符串作为秘密值,那么意义何在? SecureString是解决鸡生蛋问题的第一步,因此即使大多数当前场景需要将它们转换回常规字符串才能使用它们,它们现在存在于框架中意味着未来对它们的支持更好——至少到程序不必成为弱点的程度。

2
我在查看ProcessStartInfo的Password属性时遇到了它;我甚至没有注意到类型,只是将其设置为常规字符串,直到编译器对我发出警告。 - Richard Morgan
找到对称加密密钥并不容易,因为SecureString基于DPAPI,它并没有以明文形式存储密钥... - AviD
1
此外,这并不是鸡生蛋的问题,因为它并不是存储加密的替代品,而是对于不可变的、受管理的.NET字符串的一种解决方法。 - AviD
2
你可能已经有了明文形式的秘密值,那这样做有什么意义呢? 这个问题有答案吗?如果你打算在内存中长时间保存密码,这最多只是一个“好过没有”的解决方案。 - xr280xr
2
有没有一些简单的代码示例可以使用?我相信这样我会更好地理解何时以及如何使用它。 - codea

39

编辑: 不要使用SecureString

目前的指南表示,不应该再使用这个类。具体细节可以在此链接找到:https://github.com/dotnet/platform-compat/blob/master/docs/DE0001.md

根据这篇文章:

DE0001: 不应使用SecureString

动机

  • SecureString 的目的是避免将密钥以明文形式存储在进程内存中。
  • 然而,即使在Windows上,SecureString 也不存在于操作系统概念中。
    • 它只是缩短了获取明文的窗口时间;因为.NET仍然需要将字符串转换为明文表示形式, 所以它并没有完全防止明文泄露。
    • 好处是明文表示形式不会一直存在于System.String的实例中——本地缓冲区的寿命更短。
  • 数组的内容除了.NET Framework外都是未加密的。
    • 在.NET Framework中,内部字符数组的内容是加密的。 .NET并不支持所有环境中的加密,可能是因为缺少API或密钥管理问题等原因。

建议

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

处理凭据的一般方法是避免使用它们,而是使用安全的标识和访问控制方案。

依靠其他方式进行身份验证,例如证书或Windows身份验证。

编辑后:以下是原始摘要

有很多好的答案;这里是对讨论内容的快速概述。

微软已经实现了SecureString类,以提供更好的安全性和敏感信息(如信用卡、密码等)。它自动提供:

  • 加密(以防止内存转储或页面缓存)
  • 在内存中固定
  • 能够标记为只读(以防止进一步修改)
  • 通过不允许传递常量字符串来实现安全构造

目前,SecureString的使用受到限制,但未来可以预期更好的采用。

基于此信息,SecureString的构造函数不应该仅接受一个字符串并将其切分为字符数组,因为拼写出字符串会使SecureString失去意义。

其他信息:

  • 来自.NET Security博客的帖子,讨论了与此处涵盖的大致相同的问题。
  • 还有另一个帖子,重新审视它并提到一个可以转储SecureString内容的工具。

编辑:我发现很难选择最佳答案,因为许多答案都有很好的信息;太糟糕了,没有辅助回答选项。


21

简短回答

why can't I just say:

SecureString password = new SecureString("password");

现在您的内存中有一个 password,却没有办法将其清除 - 这正是SecureString存在的原因。

详细回答

SecureString 存在的原因是因为在使用CLR时无法使用 ZeroMemory 来清除敏感数据。它的存在是为了解决由 CLR 引起的问题。

在普通的本机应用程序中,您将调用 SecureZeroMemory

用零填充一块内存。

注意:SecureZeroMemory 与ZeroMemory相同,只是编译器不会将其优化掉。

问题在于你无法在.NET中调用ZeroMemory或SecureZeroMemory。而在.NET中,字符串是不可变的;你甚至不能像在其他语言中那样覆盖字符串的内容。请注意,这段话中有一些HTML标签。
//Wipe out the password
for (int i=0; i<password.Length; i++)
   password[i] = \0;

那么你能做什么呢?当我们使用完一个密码或信用卡号时,如何在 .NET 中提供清除内存的能力呢?

唯一的方法是将字符串放在某个本地内存块中,例如:

  • BSTR
  • HGLOBAL
  • CoTaskMem 非托管内存

然后可以调用 ZeroMemory。.NET 中的字符串无法在使用完毕后被清除:

  • 它们是不可变的;您无法覆盖其内容
  • 您无法 Dispose 它们
  • 它们的清理取决于垃圾回收器

SecureString 存在作为一种安全传递字符串的方式,并且能够在需要时保证其清理。

你问了这个问题:

why can't I just say:

SecureString password = new SecureString("password");
因为现在你在内存中有一个密码;没有办法擦除它。它会一直停留在那里,直到CLR决定重用那个内存。你让我们回到了起点;一个运行中的应用程序有一个无法摆脱的密码,并且可以通过内存转储(或进程监视器)查看密码。
SecureString使用数据保护API将字符串加密存储在内存中;这样,该字符串将不会存在于交换文件、崩溃转储甚至本地变量窗口中,即使同事正在查看您的屏幕。
如何读取密码?
然后问题来了:我该如何与字符串交互?你绝对不想要这样的方法:
String connectionString = secureConnectionString.ToString()

因为现在你又回到了最初的地方 - 一个无法摆脱的密码。你希望强制开发人员正确处理敏感字符串,以便可以从内存中擦除。这就是为什么.NET提供了三个方便的辅助函数来将SecureString编组为非托管内存的原因。 你需要将字符串转换为非托管内存块,处理它,然后再次清除它。
一些API接受安全字符串。例如,在ADO.net 4.5中,SqlConnection.Credential采用了一组SqlCredential:
SqlCredential cred = new SqlCredential(userid, password); //password is SecureString
SqlConnection conn = new SqlConnection(connectionString);
conn.Credential = cred;
conn.Open();

您还可以在连接字符串中更改密码:

SqlConnection.ChangePassword(connectionString, cred, newPassword);

有许多.NET内部的地方仍然接受普通字符串以保持兼容性,然后迅速将其放入SecureString中。

如何将文本放入SecureString中?

这仍然存在一个问题:

我该如何首先将密码存入SecureString中?

这是一个挑战,但关键是让您思考安全性。

有时功能已经为您提供。例如,WPF PasswordBox控件可以直接将输入的密码作为SecureString返回:

PasswordBox.SecurePassword Property

获取PasswordBox当前保存的密码作为SecureString

这很有帮助,因为现在你在传递原始字符串的任何地方,类型系统都会抱怨SecureString与String不兼容。在将SecureString转换回普通字符串之前,您希望尽可能长时间地保持原样。
将SecureString转换很容易:
  • SecureStringToBSTR
  • PtrToStringBSTR
例如:
private static string CreateString(SecureString secureString)
{
    IntPtr intPtr = IntPtr.Zero;
    if (secureString == null || secureString.Length == 0)
    {
        return string.Empty;
    }
    string result;
    try
    {
        intPtr = Marshal.SecureStringToBSTR(secureString);
        result = Marshal.PtrToStringBSTR(intPtr);
    }
    finally
    {
        if (intPtr != IntPtr.Zero)
        {
            Marshal.ZeroFreeBSTR(intPtr);
        }
    }
    return result;
}

他们真的不希望你这样做。

但是,如何将字符串转换为SecureString?首先,你需要停止在String中拥有密码。你需要将它放到其他地方,即使是Char[]数组也会有所帮助。

这时,当你完成操作后,你可以添加每个字符并清除明文:

for (int i=0; i < PasswordArray.Length; i++)
{
   password.AppendChar(PasswordArray[i]);
   PasswordArray[i] = (Char)0;
}

你需要将密码存储在可以清除的某个内存中。然后将其加载到SecureString中。

简单来说: SecureString 旨在提供与 ZeroMemory 相同的功能。

有些人不明白为什么需要在设备锁定后从内存中擦除用户密码或在身份验证后擦除按键记录,这些人不使用 SecureString。

其中 wiping the user's password from memory when a device is locked 意为“在设备锁定后从内存中擦除用户密码”,而 wiping keystrokes from memory after they'authenticated 则是指“在身份验证后擦除按键记录”。

15

在当前的框架版本中,使用SecureString的情景非常少。它只有在与未管理的API交互时才真正有用 - 您可以使用Marshal.SecureStringToGlobalAllocUnicode进行封送。

一旦您将其转换为System.String,就已经失去了其目的。

MSDN示例逐个字符地从控制台输入生成SecureString,并将安全字符串传递给未管理的API。这相当复杂和不切实际。

您可能期望将来的.NET版本会更多地支持SecureString,使其更加有用,例如:

  • SecureString Console.ReadLineSecure()或类似方法可将控制台输入读入SecureString,而无需在示例中编写冗长代码。

  • WinForms TextBox替代品,将其TextBox.Text属性存储为安全字符串,以便可以安全地输入密码。

  • 扩展安全相关的API,允许将密码作为SecureString传递。

若没有上述特性,SecureString的价值将受到限制。


12
我认为你必须进行字符追加而不是一次性实例化的原因是,通过将"password"传递给SecureString的构造函数,会将该字符串放入内存中,从而破坏了安全字符串的目的。
通过追加字符,你只会每次将一个字符放入内存中,这些字符很可能不会相邻,物理上使重构原始字符串变得更加困难。我可能说错了,但这就是对我解释的方式。
该类的目的是防止通过内存转储或类似工具暴露安全数据。

11

微软发现在某些导致服务器(桌面电脑等)崩溃的情况下,有时运行环境会进行内存转储,暴露出内存中的内容。Secure String会在内存中对其进行加密,以防止攻击者能够检索到字符串的内容。


5

SecureString的一个重要优点是它可以避免由于页面缓存而导致数据存储到磁盘的可能性。如果您在内存中有一个密码,然后加载一个大型程序或数据集,当您的程序从内存中换页时,您的密码可能会被写入交换文件。至少使用SecureString,数据不会以明文形式无限期地存储在您的磁盘上。


4

1
链接已失效,但似乎仍在继续:https://learn.microsoft.com/en-us/dotnet/api/system.security.securestring?view=netframework-4.8#remarks ..除了“不要使用凭据”之外,还提供了一些非常薄弱的指导,https://github.com/dotnet/platform-compat/blob/master/docs/DE0001.md..也不要使用密码来保护证书的私钥! - felickz
2
经过11年的时间,这个答案现在似乎是“新”的正确答案。链接似乎已经过时,但微软的指导是:不应该使用SecureString。 - Richard Morgan

4

我想这是因为该字符串是安全的,即黑客不能读取它。如果您使用字符串初始化它,则黑客可以读取原始字符串。


4

根据描述,该值被加密存储,这意味着您的进程的内存转储不会显示字符串的值(除非进行相当严重的工作)。

您不能仅从常量字符串构造SecureString的原因是,那样您将在内存中拥有未加密的字符串版本。限制您逐步创建字符串可减少一次性在内存中拥有整个字符串的风险。


2
如果他们限制了从常量字符串的构建,那么foreach (char c in "password".ToCharArray())这一行不就会失败吗?应该使用pass.AppendChar('p'); pass.AppendChar('a');等来代替吧? - Richard Morgan
是的,你可以很容易地放弃SecureString所提供的少量保护。他们试图让你完全自毁变得困难。显然,必须有一些方法将值传递到SecureString中并从中取出,否则你无法使用它进行任何操作。 - Mark Bessey

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接