在删除“敏感”变量之前覆盖它是一种安全的编程习惯吗?

信息安全 编程 安全编码 编译器 数据剩余
2021-08-24 05:56:19

在删除(或超出范围)之前覆盖存储在变量中的敏感数据是否是一种良好的安全编程实践?我的想法是,由于数据剩余,它可以防止黑客能够读取 RAM 中的任何潜在数据。多次覆盖它会增加安全性吗?这是我正在谈论的一个小例子,用 C++ 编写(包括注释)。

void doSecret()
{
  // The secret you want to protect (probably best not to have it hardcoded like this)
  int mySecret = 12345;

  // Do whatever you do with the number
  ...

  // **Clear out the memory of mySecret by writing over it**
  mySecret = 111111;
  mySecret = 0;
  // Maybe repeat a few times in a loop
}

一种想法是,如果这确实增加了安全性,那么如果编译器自动添加执行此操作的指令(可能是默认情况下,或者可能通过在删除变量时告诉编译器执行此操作),那就太好了。


该问题被列为本周信息安全问题
阅读 2014 年 12 月 12 日的博客文章了解更多详情或提交您自己的本周问题

4个回答

是的,覆盖然后删除/释放该值是个好主意。不要假设您所要做的就是“覆盖数据”或让它超出 GC 处理的范围,因为每种语言与硬件的交互方式不同。

在保护变量时,您可能需要考虑:

  • 加密(在内存转储或页面缓存的情况下)
  • 钉在记忆中
  • 标记为只读的能力(以防止任何进一步的修改)
  • 通过不允许传入常量字符串来安全构造
  • 优化编译器(参见链接文章re: ZeroMemory 宏中的注释)

“擦除”的实际实现取决于语言和平台。研究你使用的语言,看看是否可以安全地编码。

为什么这是个好主意?崩溃转储和任何包含堆的东西都可能包含您的敏感数据。在保护内存数据时考虑使用以下内容

有关每种语言的实施指南,请参阅 StackOverflow。

您应该知道,即使使用供应商指南(在这种情况下为 MSFT),仍然可以转储 SecureString 的内容,并且可能具有针对高安全性场景的特定使用指南。

存储不再使用的值?似乎可以优化一些东西,不管它可能提供什么好处。

此外,您可能实际上不会覆盖内存中的数据,具体取决于语言本身的工作方式。例如,在使用垃圾收集器的语言中,它不会立即被删除(这是假设您没有留下任何其他引用)。

例如,在 C# 中,我认为以下内容不起作用。

string secret = "my secret data";

...lots of work...

string secret = "blahblahblah";

"my secret data"因为它是不可变的,所以在垃圾收集之前一直存在。最后一行实际上是创建一个新字符串并为其指定秘密点。它不会加快删除实际秘密数据的速度。

有好处吗?假设我们用汇编或某种低级语言编写它,这样我们就可以确保我们正在覆盖数据,并且我们让我们的计算机进入睡眠状态或让它在应用程序运行时保持打开状态,并且我们的 RAM 被一个邪恶的女仆刮掉了,然后邪恶的女仆在秘密被覆盖之后但在它刚刚被删除之前(可能是一个非常小的空间)获得了我们的 RAM 数据,并且 RAM 或硬盘驱动器上的任何其他内容都不会泄露这个秘密......然后我看到可能增加在安全方面。

但是成本与收益似乎使这种安全优化在我们的优化列表中非常低(并且通常低于大多数应用程序的“值得”点)。

我可能会看到它在特殊芯片中的有限使用,这些芯片旨在在短时间内保存秘密,以确保它们在尽可能短的时间内保存它,但即便如此,我也不确定成本是否有任何好处。

你需要一个威胁模型

您甚至不应该开始考虑覆盖安全变量,直到您拥有一个描述您试图阻止的黑客攻击类型的威胁模型。 安全总是要付出代价的。 在这种情况下,成本是教开发人员维护所有这些额外代码以保护数据的开发成本。 这种成本意味着您的开发人员更有可能犯错误,并且这些错误更有可能是泄漏的根源,而不是内存问题。

  • 攻击者可以访问你的内存吗?如果是这样,您是否有理由认为他们不能/不会在您覆盖之前嗅探该值?攻击者可以在何种时间范围内访问您的内存
  • 攻击者可以访问核心转储吗?您是否介意他们是否可以访问敏感数据以换取噪音足以导致核心转储?
  • 这是开源的还是闭源的?如果它是开源的,你就不得不担心多个编译器,因为编译器一直优化掉诸如覆盖数据之类的东西。他们的工作不是提供安全保障。对于一个现实生活中的例子,Schneier 的 PasswordSafe 有专门的类来保护未加密的密码数据。为了做到这一点,他使用 Windows API 函数来锁定内存,并强制它被正确覆盖,而不是使用编译器来做为他
  • 这是一种垃圾收集语言吗?你知道如何强制你的特定垃圾收集器的特定版本真正摆脱你的数据吗?
  • 攻击者可以尝试多少次来获取敏感数据,然后您才注意到并使用其他手段(例如防火墙)将其切断?
  • 这是在虚拟机中运行吗?您对 Hypervisor 的安全性有多大把握?
  • 攻击者是否有物理访问权限?例如,Windows 非常乐意使用闪存驱动器来缓存虚拟内存。攻击者需要做的就是说服 Windows 将其推送到闪存驱动器上。发生这种情况时,真的很难摆脱它。事实上,如此之难,以至于没有公认的可靠地从闪存驱动器中清除数据的方法。

在考虑尝试覆盖敏感数据之前,需要解决这些问题。 试图在不解决线程模型的情况下覆盖数据是一种错误的安全感。

是的,当数据不再需要时覆盖特别敏感的数据是一种安全方面的良好做法,即作为对象析构函数的一部分(语言提供的显式析构函数或程序在解除分配之前执行的操作)目的)。覆盖本身不敏感的数据甚至是一种很好的做法,例如将不再使用的数据结构中的指针字段清零,并且即使您知道,当它们指向的对象被释放时也将指针清零您将不再使用该字段。

这样做的一个原因是,以防数据通过外部因素泄漏,例如暴露的核心转储、被盗的休眠映像、受感染的服务器允许运行进程的内存转储等。攻击者提取 RAM 棒的物理攻击除了笔记本电脑和手机等移动设备(因为焊接了 RAM,门槛更高)之外,数据剩磁的使用很少成为问题,即便如此,大多数情况下也仅限于目标场景。覆盖值的残留不是问题:在 RAM 芯片内部探测需要非常昂贵的硬件来检测可能受覆盖值影响的任何挥之不去的微观电压差。如果您担心对 RAM 的物理攻击,一个更大的问题是确保数据在 RAM 中被覆盖,而不仅仅是在 CPU 缓存中。但是,同样,这通常是一个非常次要的问题。

覆盖陈旧数据的最重要原因是为了防止导致使用未初始化内存的程序错误,例如臭名昭​​著的Heartbleed这超出了敏感数据的范围,因为风险不仅限于数据泄漏:如果存在导致指针字段在未初始化的情况下被取消引用的软件错误,则该错误既不容易被利用,也更容易追踪,如果该字段包含所有位为零,而不是它可能指向有效但无意义的内存位置。

请注意,如果检测到不再使用该值,好的编译器会优化归零。您可能需要使用一些特定于编译器的技巧来使编译器相信该值仍在使用中,从而生成归零代码。

在许多具有自动管理功能的语言中,对象可以在内存中移动而无需通知。这意味着很难控制陈旧数据的泄漏,除非内存管理器本身擦除未使用的内存(出于性能考虑,它们通常不会擦除)。从好的方面来说,您通常只需要担心外部泄漏,因为高级语言倾向于排除使用未初始化的内存(当心具有可变字符串的语言中的字符串创建函数)。

顺便说一句,我在上面写了“零输出”。您可以使用全零以外的位模式;全零的优点是它在大多数环境中都是无效指针。您知道的位模式是无效的指针,但更独特的位模式有助于调试。

许多安全标准都要求擦除密钥等敏感数据。例如,加密模块的FIPS 140-2标准即使在最低保证级别也要求它,除此之外只需要功能合规性而不是抵抗攻击。