【问题标题】:How To Properly Handle Passwords In C#如何在 C# 中正确处理密码
【发布时间】:2017-01-31 13:19:14
【问题描述】:

众所周知,C#string 非常不安全,它没有固定在 RAM 中,垃圾收集器可以移动它、复制它、在 RAM 中留下它的多个痕迹,并且 RAM 可以交换并用作一个要读取的文件,没有提及其他几个已知事实。

为了缓解这种情况,Microsoft 提出了SecureString。问题是:正确的使用方法是什么?

我问这个是因为迟早你必须提取内部字符串。是的,SecureString 确实允许我们一次写一个char 并在隐藏它的秘密的同时给我们一些其他的小用法,但有时我们需要一次整个字符串或类似的东西。到目前为止,我看到的所有实现最后都使用常规的string,从SecureString 提取和解密的内容我认为(或希望)在某些情况下是可以避免的,即使它可能涉及一些复杂的代码来解决.NET string。

在我的例子中,我使用了一个名为 BCrypt 的密码哈希库。它使用常规的string 来计算哈希值,我认为这远非理想。我没有找到任何可以直接接受SecureString 的库,所以我没有太多选择,只能使用它。

目前我运行这样的代码:

SecureString password; // This usually comes from a PasswordBox Property
IntPtr passwordBSTR = Marshal.SecureStringToBSTR(password);

string insecurePassword = Marshal.PtrToStringBSTR(passwordBSTR);
Marshal.ZeroFreeBSTR(passwordBSTR);
passwordBSTR = default(IntPtr);
password.Clear();

// Use the insecurePassword....usually as:
bool result = BCrypt.Net.BCrypt.Verify(insecurePassword, user.Password); // This hashes the provided password and compares it with another BCrypt hash.

insecureString = string.Empty; // hoping for the GC to act now, fingers crossed.

// use result and etc...

这样,BCrypt 方法的原始密码 string 将被复制多次,并且可能在其中复制更多次。

这让我想到了一些问题:

1 - 在内存管理的意义上,BSTR 字符串是否类似于 C 字符串? 它们是否固定在 RAM 中,我可以按照我认为合适的方式操作和擦除它们,所以它将是在 C# 中使用它们或与 C++ 代码互操作更安全,这样我就可以消除很多我的 .NET string 操作(例如验证密码字符串是否为空或空格或具有正确的长度或具有所需的密码强度)?

2 - 如果问题的 #1 答案是“是”,那么我是否应该花一些时间将 BSTR 字符串传递给互操作 C++ 代码以计算其哈希值并进行验证?

3 - 我后来的代码是正确的还是我在SecureString 操作上遗漏了什么?

明确一点:我对这个问题的主要目的是评估我是否应该将所有密码处理代码从我的 C# 传递给 C++(可能使用 C++/CLI 或互操作)以实现比 C# 提供的代码更安全。这种情况真的让我担心密码字符串散列,因为将创建多个字符串并将其复制,用户的秘密信息散布在整个 RAM 中。

举个例子:我可以做一个类似的方法:

public string SecureHash(SecureString password)

而且这个方法会提取SecureString值并将它作为BSTR字符串(或其他提取选项,老实说我不知道​​它们之间的好处)传递给C++哈希方法,所以我们可以控制所有我们的密码在内存中的位置。

我要求这不要进行大量工作来重构我的代码,最后发现这一切都是徒劳的,因为我一直在错误地使用SecureStrings,并且已经有解决方案,或者是错误的安全感,因为它有时会发生,因为我不知道BSTR 字符串是如何在内存或其他SecureStrings extraction options(Unicode、ANSI 等)中实现的。

【问题讨论】:

  • 您可以重写 BCrypt 以使用一个采用安全字符串的方法。根据:msdn.microsoft.com/en-us/library/… 如果您必须经常修改字符串等,实际上并不那么安全。
  • @MattClark 这正是我的想法。我的问题是值得还是不值得。我不知道 BSTR 字符串的内存行为,所以我可能会因为错误的安全感而烦恼。
  • BSTR 实际上并没有比 SecureString 获得任何好处,特别是因为 BSTR 只是非托管内存中的未加密字符串。在安全字符串和 bstr 的情况下,您可以手动对它们进行垃圾收集。
  • 我不打算通过 BSTR 从 SecureString 获得任何东西。这个想法是将 SecureString 传递给将散列它的秘密值的方法。在该方法内部将只有 BSTR 操作,因为 SecureString 值将被提取到 BSTR。但是我不确定这将如何比简单的字符串更安全,因为我不知道内存中的 BSTR 行为,即使 BSTR 是最佳选择,因为还有其他选项可以解密,例如 CoTaskMemAnsi、@987654348 @、GlobalAllocAnsi 和 GlobalAllocUnicode。
  • 如果您在 Windows 上运行,我会简单地与本地 Windows API 互操作,例如 BCryptHashData 函数,例如(有点 hacky):codereview.stackexchange.com/questions/93111/…

标签: c# c++ string security memory


【解决方案1】:
  1. Marshal.SecureStringToBSTR() 将在非托管内存中创建一个 BSTR 字符串(即字节数组)并返回一个指向它的指针;这意味着基础价值是固定的。该值本身是代表您的字符串的 Unicode 字符序列,以字符串长度为前缀,后跟两个后续空字符 (https://msdn.microsoft.com/en-us/library/windows/desktop/ms221069(v=vs.85).aspx)。 另一方面,Marshal.PtrToStringBSTR() 将返回一个不安全的 .Net System.String 对象。
  2. 我会说你的选择是:

    • 将您的 SecureString 对象转换为非托管对象,然后传递给任何 计算哈希的非托管库
    • 在您的堆栈中的任何地方都使用 SecureString,在您的情况下,这意味着重写 BCrypt.Net 以使用 SecureString 对象(该库是开源的并且非常小:https://bcrypt.codeplex.com/SourceControl/latest#BCrypt.Net/BCrypt.cs),或者找到替代的托管实现。但由于 SecureString 不提供读取其数据的方法,因此您无论如何都需要将其转换为(很可能是非托管的)字符串(不过,您可以使用托管代码中的非托管数据)。
  3. 您缺少对 SecureString 的正确处置(在“使用”或尝试/最终)中。

【讨论】:

  • 1- 如果我将其转换为 BSTR 或 Unicode,它是否会对哈希产生影响? Marshal 有一些转换选项,我不确定哪个是最好的。 2-我认为托管实现不可能是安全的,一种或另一种方式我将不得不在 C# 中提取 SecureString 的内容,这可能会导致托管代码产生其他问题,我仍在思考使用非托管代码或库是最好的,并且可能加入这两个实现,托管库接受 SecureString 并在内部将其作为非托管代码处理并返回 .NET 字符串
  • 3- 是的,我没有提到它,因为这部分有很好的文档记录。我正在使用 Clear() 方法,我已经完成了它,因为我是从 PasswordBox 中获取它的。
  • #1:如果您的算法根据您的密码计算哈希作为 字符 的序列,那么您使用什么表示(BSTR、UTF-8 等)应该没有区别正在使用。另一方面,如果您根据密码的字节 representation 计算哈希值,则会有所不同。不过我不会做后者,因为您可能会引入一些细微的错误(例如,您的密码当前在 BSTR 中,您计算​​了哈希并将它们存储在您的数据库中,然后 2 年后您的前端更改并且您开始在UTF-8,这会导致不同的哈希值,并且没有人可以再登录了)。
  • #2。你是对的,我误解了 SecureString API。我的印象是你可以一个字一个字地读。我更新了答案。
  • #1:所以当我将 .NET 字符串转换为 BSTR 或所有其他字符串时,它们的字节都将是相同的吗?只是终止符('\0''\0')会改变吗?
【解决方案2】:

为什么不使用单向加密 (HASH)?使用加密密码保存随机盐并将这两个值推送。这样,您可以使用相同的盐对要检查的密码进行加密并比较加密的值。我会推荐 SHA-256 或更高版本。

至于安全性,这与 Microsoft 在 C# AspNet Membership 中使用的方法几乎相同。由于这几乎是 Dot Net 的行业标准,因此这种安全级别也应该适合您。

这里有一个很好的资源来了解如何做到这一点: http://www.obviex.com/samples/hash.aspx

【讨论】:

  • 您提供的是 2002 年的代码表单。2016 年情况完全改变了。 BCrypt 是密码哈希存储的当前标准(PBKDF2 是第二接近的最佳选择,scrypt 也是一个很好的解决方案,只是太新而无法被大多数人信任)。 SHA256 太快了,现在你可以使用在 GPU 上运行的字典攻击找到匹配项。供参考:blog.ircmaxell.com/2011/08/rainbow-table-is-dead.html
【解决方案3】:

SecureString 的扩展方法怎么样:

using System;
using System.Runtime.InteropServices;

public static class SecureStringExtensions 
{
    public static T Decrypt<T>(this SecureString ss, Func<string, T> handler) 
    {
        if (handler == null)
        {
            throw new ArgumentNullException(nameof(handler));
        }

        var ptr = Marshal.SecureStringToBSTR(ss);
        try {
            return handler(Marshal.PtrToStringBSTR(ptr));
        } finally {
            Marshal.ZeroFreeBSTR(ptr);
        }
    }
}

这样,解密后的字符串会保留在堆栈中,并很快被未来的堆栈变量覆盖。

【讨论】:

  • 这不是问题...问题是 C# 的所有 bcrypt 框架都使用不安全的字符串...您的代码只是从 SecureString 中提取 BSTR 字符串的一种封装方式已经可以处理 BSTR 字符串...只是一个辅助方法,但它不会散列任何密码字符串或按要求做任何特殊的事情...我无法使用它获得 bcrypt 散列,除了下属 BSTR 字符串,这不是问题的全部内容。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2011-02-16
  • 1970-01-01
  • 2013-11-19
  • 1970-01-01
  • 1970-01-01
  • 2022-04-14
  • 1970-01-01
相关资源
最近更新 更多