题 如何确保编译器优化不会带来安全风险?


我必须编写一个Windows服务,在某些时候处理机密数据(如PIN码,密码等)。这些信息需要很短的时间:通常它们几乎立即被发送到智能卡读卡器。

让我们考虑这段代码:

{
  std::string password = getPassword(); // Get the password from the user

  writePasswordToSmartCard(password);

  // Okay, here we don't need password anymore.
  // We set it all to '\0' so it doesn't stay in memory.
  std::fill(password.begin(), password.end(), '\0');
}

现在我关心的是编译器优化。在这里,编译器可能会检测到密码即将被删除,并且此时更改其值是无用的,只需删除该调用即可。

我不希望我的编译器关心未来未引用的内存的价值。

我的担忧是否合法?我怎么能确定这样的代码不会被优化?


38
2017-09-24 08:23


起源




答案:


是的,您的担忧是合法的。你需要使用专门设计的功能 SecureZeroMemory() 防止优化修改您的代码行为。

不要忘记字符串类应该是专门为处理密码而设计的。例如,如果类重新分配缓冲区以保存更长的字符串,则必须先擦除缓冲区,然后再将其重新调整到内存分配器。我不确定,但很可能 std::string 不这样做(至少在默认情况下)。使用不合适的字符串处理类会使您的所有问题变得毫无价值 - 您甚至可以在程序存储器中复制密码。


33
2017-09-24 08:26



谢谢。我只是想知道这个函数是如何工作的:什么阻止编译器优化它? - ereOn
@ereOn:这很简单,它的代码在程序编译时没有呈现给编译器,因此编译器无法看到它并决定它“没有任何用处”。例如,它可以编译成DLL,并且只能动态链接到。 - sharptooth
@sharptooth:确实有道理。我会尽快接受这个;)你有关于编写安全密码处理类的链接/教程吗? - ereOn
std::string 只是 std::basic_string<char, std::char_traits<char>, std::alloc>。我认为分配器应该是'正确'的位置,足以处理安全归零任何即将被释放的内存。 - Christopher Creutzig
@sharptooth:当字符串对象被销毁时,将重新分配或释放内存。将要求分配器执行其中任何一个操作,并且可以执行操作系统或运行时提供的任何安全归零操作。是的,缩短字符串可能会导致某些数据长时间不必要 - 但绝不会像可能重复使用的空闲内存那样。 (无论如何缩短了什么密码字符串?) - Christopher Creutzig


这是有问题的,但另一个原因。谁说的 std::string password = getPassword(); 不会在记忆中留下另一份副本吗? (可能你需要为“破坏”存储器“destruct”或“deallocate”写一个“安全”的分配器类)

在您的代码安静中,您可以通过获取指向字符串数据的易失性指针(我不知道您是否可以以标准方式执行)来避免优化,然后将数据归零。


9
2017-09-24 08:30



好吧, getPassword() 事情刚刚编写我的示例代码。但你绝对正确,也必须关心这一点。 - ereOn
@ereOn无论您生成密码的方式如何,在这种情况下都必须非常小心。最简单的方法是使用自定义分配器,通过上述方法之一(volatile或SecureZeroMemory)对内存释放内存。请注意,std :: string不会破坏其元素,只会解除分配。 - ybungalobill


 使用 std::string 对于密码,因为在进行重新分配或销毁时它不会将内存归零 - 设计自己的密码 ConfidentialString 而是改为。在设计该课程时,您可能希望利用 CryptProtectMemory...当你需要使用解密版本时,非常非常小心,特别是在调用外部代码时。


7
2017-09-24 09:24



我不知道那个功能。谢谢,这肯定会有所帮助。 - ereOn
您不必从头开始创建自己的字符串类,只需创建自己的字符串类 分配器。
@Roger Pate:你可能还是更好地自己滚动 - 让你更容易控制你可以(并且不能!)传递密码的外部API,让你在内部保持字符串加密,并避免意外的复制构造。此外,您需要多长时间对密码进行字符串操作? :) - snemarch


在这个特定的例子中,如果编译器可以优化远离可能明显具有副作用的方法调用,我会感到非常惊讶。或者是std :: fill inline所以编译器可以看到实现? (我不是C ++程序员)。

话虽如此,这种事情一般都是一个问题。但是你需要考虑利用它是多么容易。要读取另一个进程的内存,攻击者需要一定级别的管理员访问权限(如果没有,为什么要使用该操作系统)。如果机器被泄露到那个级别,你就已经输了。


1
2017-09-24 08:54



我倾向于同意。但是,在我的位置,当软件崩溃时,转储文件将被发送到特定服务或开发人员自己。即使这些人可以信任,如果我可以避免将机密数据存储在某个地方,那就更好了。 - ereOn
@ereOn:我担心转储文件,如果它包含进程的映像,本身就是一个安全风险。您没有做任何事情来消除敏感数据可以保护您,因为开发人员可以访问代码并且可以轻易地破坏您的安全措施。 - JeremyP
开发人员无法更改在我们的生产环境下运行的代码。如果代码安全且安全,那么了解它的工作方式无助于破解数据。如果您设法从转储中删除所有敏感数据,那些也无济于事。 - ereOn
如果有一个平台,我会感到惊讶 std::fill() 是 不内联。事实上,我希望我的实现能够回归到某些特定于平台的内在特性。编译器当然应该能够优化它。 - sbi
因为std :: fill是一个模板,所以它可以内联。 (即使使用导出的模板,在C ++ 0x中删除,也必须有一些东西可以生成正确的代码。)


为什么不禁用相关代码的优化?

#pragma optimize( "", off )

// Code, not to optimize goes here

#pragma optimize( "", on )

此#pragma optimize示例特定于MSVC,但其他编译器也支持它。


0
2017-09-24 08:40





声明密码volatile以防止编译器对删除显式读取或写入做出任何假设。

volatile std::string password = getPassword(); // Get the password from the user

-1
2017-09-24 08:37



我可能错了,但我相信 std::string 不是设计的 volatile:它的所有方法都没有声明 volatile 所以我无法打电话给他们。 - ereOn
@ereOn:也许最安全的解决方案是使用C字符串,您可以完全控制用于存储字符串的内存。 - Clifford
是的。如果最终没有什么我觉得足够舒服,我肯定会最终使用C字符串。 - ereOn