【问题标题】:How-to ensure that compiler optimizations don't introduce a security risk?如何确保编译器优化不会带来安全风险?
【发布时间】:2010-09-24 08:23:38
【问题描述】:

我必须编写一个 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');
}

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

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

我的担忧合理吗?我如何确定这样一段代码不会被优化出来?

【问题讨论】:

    标签: c++ security optimization memory compiler-construction


    【解决方案1】:

    是的,您的担忧是合理的。您需要使用专门设计的函数,例如SecureZeroMemory(),以防止优化修改您的代码行为。

    不要忘记字符串类应该是专门为处理密码而设计的。例如,如果类重新分配缓冲区以保存更长的字符串,则它必须在将缓冲区返回给内存分配器之前擦除缓冲区。我不确定,但std::string 可能不会这样做(至少默认情况下)。使用不合适的字符串处理类会使您的所有顾虑都变得毫无价值——您甚至会在不知不觉中将密码复制到整个程序内存中。

    【讨论】:

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

    这是有问题的,但有另一个原因。谁说std::string password = getPassword(); 不会在内存中留下另一个副本? (可能您需要为此编写一个“安全”分配器类,在“destruct”或“deallocate”时将内存归零)

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

    【讨论】:

    • 好吧,getPassword() 只是为了编写我的示例代码。但你说得对,你也必须关心这一点。
    • @ereOn 无论您以何种方式生成密码,在这种情况下都必须非常小心。最简单的方法是使用自定义分配器,通过上述方法之一(volatile 或 SecureZeroMemory)将释放的内存归零。请注意,std::string 不会破坏其元素,只会释放。
    【解决方案3】:

    不要使用std::string 作为密码,因为它不会在重新分配或销毁时将其内存归零 - 请设计您自己的ConfidentialString 类。在设计该类时,您可能希望利用 CryptProtectMemory... 并在需要使用解密版本时非常非常小心,尤其是在调用外部代码时。

    【讨论】:

    • 我不知道这个功能。谢谢,一定会有所帮助。
    • 您不必从头开始发明自己的字符串类,只需您自己的allocator
    • @Roger Pate:您最好还是自己动手 - 让您更轻松地控制可以(也不能!)将密码传递给哪些外部 API,让您保留字符串内部加密,并避免意外的复制构造。此外,您需要多久对密码短语进行一次字符串操作? :)
    【解决方案4】:

    在这个特定的例子中,如果编译器可以优化掉一个明显会产生副作用的方法调用,我会感到非常惊讶。还是 std::fill 内联以便编译器可以看到实现? (我不是 C++ 程序员)。

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

    【讨论】:

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

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

    #pragma optimize( "", off )
    
    // Code, not to optimize goes here
    
    #pragma optimize( "", on )
    

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

    【讨论】:

      猜你喜欢
      • 2012-08-19
      • 2010-12-28
      • 2016-07-02
      • 2011-08-22
      • 2012-06-09
      • 1970-01-01
      • 2011-05-06
      • 2013-05-03
      • 2016-09-25
      相关资源
      最近更新 更多