如何安全地清除std::string?

25

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

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

如果我将密码存储为 char * 数组,则可以使用诸如 SecureZeroMemory 等API从进程内存中清除敏感数据。然而,我想避免在我的代码中使用 char 数组,并寻找类似于 std::string 的东西?


1
根据此链接,std::strings并非为安全目的而设计。 - Marlon
谢谢Marlon,这意味着我别无选择,只能在我的方法接口中添加char *buf, size_t len :) - ajd.
@user34965:不是那么二元的。你应该设计一个class SecureString。最好复制std :: string的接口,这样它可以直接替换。 - MSalters
7个回答

16

根据这里给出的答案,我编写了一个分配器以安全地清零内存。

#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的实现方式,对于小值可能根本不会调用分配器。例如,在我的代码中,对于字符串bar(在Visual Studio中),甚至没有调用deallocate

因此,答案是我们不能使用std::string来存储敏感数据。当然,我们可以编写一个新类来处理这种情况,但我特别想使用定义好的std::string

感谢大家的帮助!


9

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;
}

6
这是一个复杂的话题,因为优化编译器会对你产生反作用。像循环字符串并覆盖每个字符这样的直接方法不可靠,因为编译器可能会将其优化掉。同样,memset也是如此,但是C11添加了memset_s,它应该是安全的,但可能无法在所有平台上使用。
因此,我强烈建议使用受信任的加密库来完成此任务,并让他们的作者负责可移植性。安全擦除是一项基本操作(将C数组覆盖为安全状态),所有库都必须在某个时候实现。请注意,std::string中的底层数据是连续的(由C++11标准规定,但实际上即使在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()而不是&data[0],因为前者保证了字符串缓冲区已准备好以连续模式读取。然而,他们说1)通过data的const重载访问修改字符数组具有未定义行为。但我很确定OPENSSL_cleanse(&secret[0], secret_str.size());已经是UB了,所以,嗯。 - quetzalcoatl
在大量阅读之后,我最终失去了信心,并询问了https://dev59.com/im025IYBdhLWcg3w9qyQ#sBHsnYgBc1ULPQZFQAC9,我们将看到另一个判断XD。 - quetzalcoatl
2
重要的是强调:如果您的std::string经历了复制或调整大小,仅使用诸如OPENSSL_cleansememset_zmemset_s之类的东西来擦除std::string是不够的。 - josaphatv

4

为了纪念,我曾经决定忽略这个建议并且仍旧使用std::string,编写了一个零()方法,使用c_str()(并强制转换成volatile)。如果我小心一些,不会导致重分配/移动内容,而且在需要清除时手动调用zero(),所有情况似乎都可以正常运行。可惜,我发现另一个严重缺陷:std::string也可以是一个引用计数的对象……在c_str()(或引用对象指向的内存)处炸毁内存将不知不觉地炸毁其他对象。


6
自C++11起,引用计数实现已经被禁止。 - Baum mit Augen
1
@BaummitAugen 现在是不合法的,但值得注意的是,如果有人使用旧编译器。话说,通过字符串的.data()或.c_str()指针覆盖数据的合法性如何?最近标准版本中是否已将其更改为非UB? - quetzalcoatl

0

适用于Windows

std::string s("ASecret");
const char* const ptr = s.data();
SecureZeroMemory((void*)ptr, s.size());

这将根据STL内部机制,安全地清除堆栈或堆中的数据。

适用于所有大小的字符串,无论是小还是大。

注意!

请勿使用ptr来更改字符串的数据,否则可能会导致长度增加或减少。


-1

std::string基于char*。在所有动态魔法的背后,实际上是一个char*。因此,当你说你不想在你的代码中使用char*时,你仍然在使用char*,只是在背景下有一堆其他垃圾堆积在其上。

我对进程内存不是很有经验,但你可以迭代每个字符(在加密并将密码存储在数据库之后),并将其设置为不同的值。

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


1
仅手动覆盖每个字符是不够的 - 因为编译器可以优化这样的代码,如果字符串即将被销毁。请参见Marlon在上面评论中链接的问题 - ajd.
然后覆盖每个字符,最后使用字符串 ;) - Andrew Rasmussen
2
std::string中覆盖任何内容的主要问题是,世界上没有任何保证它实际上会重写字符串所在的内存,或者它是否重写了字符串曾经存在的所有内存,因为std::string可能会移动底层缓冲区。 - Jan Hudec
有一些汇编指令可以包含在内,告诉系统和编译器任何东西都可以从另一个线程读取该数据。这将防止优化,但也会是特定于平台的。 - Sqeaky
关于“它只是一个char*”,这些观点并不有用。std::stringstd::basic_string相同。而且,当通过某些专门的机制进行覆盖字符时,“仅仅覆盖字符”的安全影响的其余部分是危险的错误信息。使用专门构建的类替代std::string或者使用带有memset_s(或类似的)的char* - josaphatv

-2
std::string mystring;
...
std::fill(mystring.begin(), mystring.end(), 0);

或者更好的是编写你自己的函数:

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

6
无法奏效。无法保证你已覆盖字符串曾经所在的所有内存,因为它可能在某些操作期间被移动。 - Jan Hudec
5
高智能的优化器也可以检测到您从未再次使用零,并跳过填充。 - Bo Persson

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接