从内存中清除 C# 字符串
Clear C# String from memory
出于安全原因,我正在尝试清除 C# 字符串的内存内容。
我知道 SecureString
class,但不幸的是我不能在我的应用程序中使用 SecureString
而不是 String
。需要清除的字符串是在运行时动态创建的(例如,我不是要清除字符串文字)。
我发现的大多数搜索结果基本上都是说清除 String
的内容是不可能的(因为字符串是不可变的),应该使用 SecureString
。
因此,我确实在下面提出了自己的解决方案(使用不安全代码)。测试表明解决方案有效,但我仍然不确定解决方案是否有问题?还有更好的吗?
static unsafe bool clearString(string s, bool clearInternedString=false)
{
if (clearInternedString || string.IsInterned(s) == null)
{
fixed (char* c = s)
{
for (int i = 0; i < s.Length; i++)
c[i] = '[=10=]';
}
return true;
}
return false;
}
编辑: 由于 GC 在调用 clearString
之前移动字符串的评论:以下代码段如何?
string s = new string('[=11=]', len);
fixed (char* c = s)
{
// copy data from secure location to s
c[0] = ...;
c[1] = ...;
...
// do stuff with the string
// clear the string
for (int i = 0; i < s.Length; i++)
c[i] = '[=11=]';
}
你的问题是字符串可以移动。如果 GC 运行,它可以将内容移动到新位置,但不会将旧位置清零。如果您确实将有问题的字符串清零,则无法保证它的副本不存在于内存中的其他位置。
这是 .NET 垃圾收集器的 link,它讨论了压缩。
编辑:
这是您的更新问题:
// do stuff with the string
问题在于,一旦它脱离了您的控制,您就失去了确保其安全的能力。如果它完全在您的控制之下,那么您将不会受到仅使用字符串类型的限制。简而言之,这个问题已经存在很长时间了,而且还没有人想出一个安全的方法来处理这个问题。如果您想保证它的安全,最好通过其他方式处理。清除字符串是为了防止有人通过内存转储找到它。如果您不能使用安全字符串,最好的停止方法是限制对代码 运行 所在机器的访问。
除了标准的 "You're stepping into unsafe territory" 答案(我希望它能自我解释)之外,请考虑以下内容:
CLR 不保证在任何给定点只有一个字符串实例,也不保证字符串将被垃圾回收。如果我要执行以下操作:
var input = "somestring";
input += "sensitive info";
//do something with input
clearString(input, false);
这样做的结果是什么? (假设我没有使用字符串文字,而是来自某种环境的输入)
使用 "somestring" 的内容创建了一个字符串。另一个字符串是用 "sensitive info" 的内容创建的,另一个字符串是用 "somestringsensitive info" 的内容创建的。只有后一个字符串被清除: "sensitive info" 不是。它可能会或可能不会立即被垃圾收集。
即使您小心确保始终清除任何包含敏感信息的字符串,CLR 仍然不能保证只存在一个字符串实例。
编辑:
关于您的编辑,只需立即固定字符串可能会产生预期的效果 - 无需将字符串复制到其他位置或任何其他位置。您确实需要在收到所述字符串后立即执行此操作,并且还有其他安全问题需要担心。你不能保证,例如,字符串的来源在它的内存中没有它的副本,没有清楚地理解来源和它究竟是如何做的。
出于明显的原因,您也将无法改变此字符串(除非改变后的字符串与字符串的大小完全相同),并且您需要非常小心,以防您正在做的任何事情都不会受到影响不属于该字符串的内存。
此外,如果您将它传递给其他不是您自己编写的函数,它可能会也可能不会被该函数复制。
无法判断您的字符串在到达您尝试清除它的函数之前经过了多少个 CLR 和非 CLR 函数。这些函数(托管和非托管)可能会出于各种原因(可能是多个副本)创建字符串的副本。
你不可能知道所有这些地方并如此真实地清除它们,你不能保证你的密码从记忆中清除。您 应该 改用 SecureString
但您需要了解以上内容仍然适用:在您的程序中的某个时刻您将收到密码并且您必须将其输入内存(即使只是在您将其移动到安全字符串中时的一小段时间内)。这意味着您的字符串仍将通过您无法控制的函数调用链。
如果你真的无法使用 SecureString
,并且你愿意编写不安全的代码,那么你可以编写自己的简单字符串 class,它使用非托管内存并确保所有内存在释放之前被清零。
但是,您永远无法真正确保您的数据安全,因为您永远无法完全控制它。例如,嵌入足够深的病毒可以在程序 运行ning 时读取该内存,这也是进程终止的可能性,在这种情况下,析构函数代码不会 运行,将数据留在未分配的内存中,该内存可以分配给另一个进程,并且它最初仍将包含您的敏感数据;有人可以轻松地使用诸如 visual studio 之类的工具来监视被调试进程的内存,或者编写一个程序来分配内存并在其中搜索敏感数据。
作为 SecureString 的用户,我有时会从常规字符串中获取输入,并且一旦我将其放入 SecureString 中,就会将传入的字符串内存固定为零,就像您正在做的那样。
然后我 运行 遇到了一个奇怪的错误,其中来自第 3 方库 (Redis) 的内存被归零。事实证明,第 3 方库有两个字符串实例,其内容与测试输入的常规字符串 ("password") 完全相同。显然.NET 优化了所有 3 个字符串以指向相同的内存缓冲区。因此,当我固定字符串的 'own' 内存并将其归零时,结果我也将第 3 方库内存归零。然后 Redis 客户端库无法解析连接字符串,错误是 "password" 不是可识别的键。
所以我从惨痛的教训中学到的教训是不要将一个字符串的内存归零,因为它也可能是另一个具有相同内容的字符串的内存。
出于安全原因,我正在尝试清除 C# 字符串的内存内容。
我知道 SecureString
class,但不幸的是我不能在我的应用程序中使用 SecureString
而不是 String
。需要清除的字符串是在运行时动态创建的(例如,我不是要清除字符串文字)。
我发现的大多数搜索结果基本上都是说清除 String
的内容是不可能的(因为字符串是不可变的),应该使用 SecureString
。
因此,我确实在下面提出了自己的解决方案(使用不安全代码)。测试表明解决方案有效,但我仍然不确定解决方案是否有问题?还有更好的吗?
static unsafe bool clearString(string s, bool clearInternedString=false)
{
if (clearInternedString || string.IsInterned(s) == null)
{
fixed (char* c = s)
{
for (int i = 0; i < s.Length; i++)
c[i] = '[=10=]';
}
return true;
}
return false;
}
编辑: 由于 GC 在调用 clearString
之前移动字符串的评论:以下代码段如何?
string s = new string('[=11=]', len);
fixed (char* c = s)
{
// copy data from secure location to s
c[0] = ...;
c[1] = ...;
...
// do stuff with the string
// clear the string
for (int i = 0; i < s.Length; i++)
c[i] = '[=11=]';
}
你的问题是字符串可以移动。如果 GC 运行,它可以将内容移动到新位置,但不会将旧位置清零。如果您确实将有问题的字符串清零,则无法保证它的副本不存在于内存中的其他位置。
这是 .NET 垃圾收集器的 link,它讨论了压缩。
编辑: 这是您的更新问题:
// do stuff with the string
问题在于,一旦它脱离了您的控制,您就失去了确保其安全的能力。如果它完全在您的控制之下,那么您将不会受到仅使用字符串类型的限制。简而言之,这个问题已经存在很长时间了,而且还没有人想出一个安全的方法来处理这个问题。如果您想保证它的安全,最好通过其他方式处理。清除字符串是为了防止有人通过内存转储找到它。如果您不能使用安全字符串,最好的停止方法是限制对代码 运行 所在机器的访问。
除了标准的 "You're stepping into unsafe territory" 答案(我希望它能自我解释)之外,请考虑以下内容:
CLR 不保证在任何给定点只有一个字符串实例,也不保证字符串将被垃圾回收。如果我要执行以下操作:
var input = "somestring";
input += "sensitive info";
//do something with input
clearString(input, false);
这样做的结果是什么? (假设我没有使用字符串文字,而是来自某种环境的输入)
使用 "somestring" 的内容创建了一个字符串。另一个字符串是用 "sensitive info" 的内容创建的,另一个字符串是用 "somestringsensitive info" 的内容创建的。只有后一个字符串被清除: "sensitive info" 不是。它可能会或可能不会立即被垃圾收集。
即使您小心确保始终清除任何包含敏感信息的字符串,CLR 仍然不能保证只存在一个字符串实例。
编辑: 关于您的编辑,只需立即固定字符串可能会产生预期的效果 - 无需将字符串复制到其他位置或任何其他位置。您确实需要在收到所述字符串后立即执行此操作,并且还有其他安全问题需要担心。你不能保证,例如,字符串的来源在它的内存中没有它的副本,没有清楚地理解来源和它究竟是如何做的。
出于明显的原因,您也将无法改变此字符串(除非改变后的字符串与字符串的大小完全相同),并且您需要非常小心,以防您正在做的任何事情都不会受到影响不属于该字符串的内存。
此外,如果您将它传递给其他不是您自己编写的函数,它可能会也可能不会被该函数复制。
无法判断您的字符串在到达您尝试清除它的函数之前经过了多少个 CLR 和非 CLR 函数。这些函数(托管和非托管)可能会出于各种原因(可能是多个副本)创建字符串的副本。
你不可能知道所有这些地方并如此真实地清除它们,你不能保证你的密码从记忆中清除。您 应该 改用 SecureString
但您需要了解以上内容仍然适用:在您的程序中的某个时刻您将收到密码并且您必须将其输入内存(即使只是在您将其移动到安全字符串中时的一小段时间内)。这意味着您的字符串仍将通过您无法控制的函数调用链。
如果你真的无法使用 SecureString
,并且你愿意编写不安全的代码,那么你可以编写自己的简单字符串 class,它使用非托管内存并确保所有内存在释放之前被清零。
但是,您永远无法真正确保您的数据安全,因为您永远无法完全控制它。例如,嵌入足够深的病毒可以在程序 运行ning 时读取该内存,这也是进程终止的可能性,在这种情况下,析构函数代码不会 运行,将数据留在未分配的内存中,该内存可以分配给另一个进程,并且它最初仍将包含您的敏感数据;有人可以轻松地使用诸如 visual studio 之类的工具来监视被调试进程的内存,或者编写一个程序来分配内存并在其中搜索敏感数据。
作为 SecureString 的用户,我有时会从常规字符串中获取输入,并且一旦我将其放入 SecureString 中,就会将传入的字符串内存固定为零,就像您正在做的那样。 然后我 运行 遇到了一个奇怪的错误,其中来自第 3 方库 (Redis) 的内存被归零。事实证明,第 3 方库有两个字符串实例,其内容与测试输入的常规字符串 ("password") 完全相同。显然.NET 优化了所有 3 个字符串以指向相同的内存缓冲区。因此,当我固定字符串的 'own' 内存并将其归零时,结果我也将第 3 方库内存归零。然后 Redis 客户端库无法解析连接字符串,错误是 "password" 不是可识别的键。 所以我从惨痛的教训中学到的教训是不要将一个字符串的内存归零,因为它也可能是另一个具有相同内容的字符串的内存。