【问题标题】:how does one securely clear std::string?如何安全地清除 std::string?
【发布时间】:2011-08-07 13:36:58
【问题描述】:

如何在std::string 中存储敏感数据(例如:密码)?

我有一个应用程序,它提示用户输入密码并在连接设置期间将其传递给下游服务器。我想在建立连接后安全地清除密码值。

如果我将密码存储为char * 数组,我可以使用SecureZeroMemory 之类的API 从进程内存中删除敏感数据。但是,我想在我的代码中避免使用 char 数组,并且正在为 std::string 寻找类似的东西?

【问题讨论】:

  • 根据this link,std::strings 不是为安全目的而设计的。
  • 感谢 Marlon,这意味着我别无选择,只能将我的方法接口与 char *buf, size_t len 混淆:)
  • @user34965:这不是二进制文件。你应该设计一个class SecureString。复制std::string的接口是个好主意,这样就可以直接替换了。

标签: c++ string passwords secure-coding


【解决方案1】:

根据here 给出的答案,我编写了一个分配器来安全地归零内存。

#include <string>
#include <windows.h>

namespace secure
{
  template <class T> class allocator : public std::allocator<T>
  {
  public:

    template<class U> struct rebind { typedef allocator<U> other; };
    allocator() throw() {}
    allocator(const allocator &) throw() {}
    template <class U> allocator(const allocator<U>&) throw() {}

    void deallocate(pointer p, size_type num)
    {
      SecureZeroMemory((void *)p, num);
      std::allocator<T>::deallocate(p, num);
    }
  };

  typedef std::basic_string<char, std::char_traits<char>, allocator<char> > string;
}

int main()
{
  {
    secure::string bar("bar");
    secure::string longbar("baaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaar");
  }
}

然而,事实证明,根据std::string 的实现方式,分配器可能甚至不会为小值调用。例如,在我的代码中,deallocate 甚至不会被调用字符串 bar(在 Visual Studio 上)。

因此,答案是我们不能使用 std::string 来存储敏感数据。当然,我们可以选择编写一个处理用例的新类,但我对使用定义的 std::string 特别感兴趣。

感谢大家的帮助!

【讨论】:

    【解决方案2】:

    openssl 经历了几次安全擦除字符串的迭代,直到它确定了这种方法:

    #include <string.h>
    #include <string>
    
    // Pointer to memset is volatile so that compiler must de-reference
    // the pointer and can't assume that it points to any function in
    // particular (such as memset, which it then might further "optimize")
    typedef void* (*memset_t)(void*, int, size_t);
    
    static volatile memset_t memset_func = memset;
    
    void cleanse(void* ptr, size_t len) {
      memset_func(ptr, 0, len);
    }
    
    int main() {
      std::string secret_str = "secret";
      secret_str.resize(secret_str.capacity(), 0);
      cleanse(&secret_str[0], secret_str.size());
      secret_str.clear();
    
      return 0;
    }
    

    【讨论】:

      【解决方案3】:

      这是一个复杂的话题,就像optimizing compiler will work against you。像循环遍历字符串和覆盖每个字符这样的简单方法并不可靠,因为编译器可能会将其优化掉。与memset 相同,但是,C11 添加了memset_s,它应该是安全的,但可能并非在所有平台上都可用。

      因此,我强烈建议为该任务使用受信任的加密库,并让其作者负责可移植性。安全擦除是一项基本操作(获取 C 数组并安全地覆盖它),所有库都必须在某个时候实现。请注意,std::string 中的基础数据是连续的(如mandated by the C++11 standard,但实际上即使在 C++98/03 中你也可以假设它)。因此,您可以通过将std::string 作为一个数组来使用加密库的安全擦除功能。

      在 OpenSSL 中,安全擦除由 OPENSSL_cleanse 函数提供。 Crypto++有memset_z:

      std::string secret;
      // ...
      
      // OpenSSL (#include <openssl/crypto.h> and link -lcrypto)
      OPENSSL_cleanse(&secret[0], secret_str.size());
      
      // Crypto++ (#include <crypto++/misc.h> and link -lcrypto++)
      CryptoPP::memset_z(&secret[0], 0, secret.size());
      

      附带说明,如果您从头开始设计 API,请考虑在存储机密时完全避免使用 std::stringstd::string 的设计目标不是防止泄露秘密(或在调整大小或复制期间泄露秘密)。

      【讨论】:

      • 我认为最好使用secret.data() 而不是&amp;data[0],因为前者可以保证字符串的缓冲区已准备好以连续模式读取。但是,they say1) Modifying the character array accessed through the const overload of data has undefined behavior.,但我很确定OPENSSL_cleanse(&amp;secret[0], secret_str.size()); 已经是 UB,所以,好吧。
      • 看了很多,终于没信心了,问stackoverflow.com/questions/5698002/…,我们再看判断XD
      • 重要的是要强调:如果你的 std::string 经历了复制或调整大小,使用 OPENSSL_cleansememset_zmemset_s 之类的东西是不够的.
      【解决方案4】:

      为了后代,我曾经决定忽略这个建议并使用 std::string ,并使用 c_str() (并抛弃 constness)和 volatile 编写了 zero() 方法。如果我很小心并且没有导致内容的重新分配/移动,并且我在需要清理的地方手动调用了 zero(),那么一切似乎都可以正常运行。唉,我发现了另一个严重的缺陷:std::string 也可以是一个引用计数的对象......在 c_str() 处爆破内存(或被引用对象指向的内存)会在不知不觉中爆破另一个对象.

      【讨论】:

      • 从 C++11 开始,引用计数实现是非法的。
      • @BaummitAugen 现在是非法的,但如果有人使用旧编译器,请注意。无论如何,通过字符串的 .data() 或 .c_str() 指针覆盖数据的合法性如何?在最新版本的标准中是否已将其更改为非 UB?
      【解决方案5】:

      std::string 基于 char*。在所有动态魔法背后的某个地方,就像一个 char*。所以当你说你不想在你的代码中使用 char* 时,你仍然在使用 char*,它只是在后台,上面堆着一大堆其他垃圾。

      我对进程内存不太熟悉,但您总是可以遍历每个字符(在加密并将密码存储在数据库中之后?),并将其设置为不同的值。

      还有一个 std::basic_string,但我不确定它会对你有什么帮助。

      【讨论】:

      • 不能手动覆盖每个字符 - 因为如果字符串即将被销毁,编译器可以优化此类代码。请参阅上面评论中链接到的question Marlon。
      • 然后覆盖每个字符,然后使用字符串;)
      • std::string 中覆盖任何内容的主要问题是,世界上没有任何事情可以保证它会真正重写字符串所在的内存,或者它会重写字符串曾经所在的所有内存, 因为std::string 可能会移动底层缓冲区。
      • 可以包含一些汇编指令来告诉系统和编译器任何东西都可以从另一个线程读取该数据。这会阻止优化,但也会因平台而异。
      • 关于“它只是引擎盖下的 char*”的观点没有用。 std::stringstd::basic_string 相同。当没有通过某种专门的机制完成时,答案的其余部分被危险地误导了“仅覆盖字符”的安全含义。使用专门构建的类而不是 std::string 或使用 char*memset_s(或类似的东西)。
      【解决方案6】:
      std::string mystring;
      ...
      std::fill(mystring.begin(), mystring.end(), 0);
      

      甚至更好地编写自己的函数:

      void clear(std::string &v)
      {
        std::fill(v.begin(), v.end(), 0);
      }
      

      【讨论】:

      • 行不通。无法保证您覆盖了字符串曾经所在的所有内存,因为它可能在某些操作期间被移动了。
      • 一个足够聪明的优化器也可以检测到你不再使用零,并跳过填充。
      猜你喜欢
      • 1970-01-01
      • 2017-01-10
      • 1970-01-01
      • 2017-04-06
      • 1970-01-01
      • 2023-01-11
      • 2018-01-27
      • 2021-07-21
      • 1970-01-01
      相关资源
      最近更新 更多