如何清理(用随机字节覆盖)std :: string内部缓冲区?

hau*_*ron 25 c++ stdstring c++11

考虑一个场景,std::string用于存储秘密.一旦它被消耗并且不再需要它,最好清理它,即覆盖包含它的内存,从而隐藏秘密.

std::string提供一个函数const char* data()返回指向(自C++ 11)连续内存的指针.

现在,由于内存是连续的,并且由于范围结束,变量将在清理后立即销毁,因此安全:

char* modifiable = const_cast<char*>(secretString.data());
OpenSSL_cleanse(modifiable, secretString.size());
Run Code Online (Sandbox Code Playgroud)

根据这里引用的标准:

$ 5.2.11/7 -注意:根据目的,通过指针,左值或指针以从得到的数据成员的写操作的类型const_cast即擅自抛弃一个const-qualifier68可能会产生不确定的行为(7.1.5.1).

否则会提出建议,但是上面的条件(连续的,待去除的)是否安全?

Gal*_*lik 17

这可能是安全的.但不保证.

然而,因为C++11,一个std::string必须这样你就可以使用它的第一个元素的地址安全存取其内部的阵列实现为邻接数据&secretString[0].

if(!secretString.empty()) // avoid UB
{
    char* modifiable = &secretString[0];
    OpenSSL_cleanse(modifiable, secretString.size());
}
Run Code Online (Sandbox Code Playgroud)

  • @hauron以读写元件的地址必须产生一个读写指针.我没有看到编译器如何搞乱这个概念tbh. (7认同)
  • 怎么可能在只读段中的数据结束时,它从堆中分配在一个连续的块,并写入(存储字符串内容后空终止)?你得到了`字符*`这样,所以你可以写它.标准说,通过`data()`返回的`const char*`来编写它是未定义的.一个显然是安全,一个是明确的定义.毫无疑问应该使用哪个. (6认同)

Jon*_*ely 14

标准明确表示你不得写入const char*返回的data(),所以不要这样做.

有一种非常安全的方法可以获得可修改的指针:

if (secretString.size())
  OpenSSL_cleanse(&secretString.front(), secretString.size());
Run Code Online (Sandbox Code Playgroud)

或者,如果字符串可能已经缩小,并且您希望确保其整个容量被擦除:

if (secretString.capacity()) {
  secretString.resize(secretString.capacity());
  OpenSSL_cleanse(&secretString.front(), secretString.size());
}
Run Code Online (Sandbox Code Playgroud)

  • 这个,再加上确保你为整个密码"保留"足够的空间,然后将其输入到字符串中(或放大缓冲区可能会留下部分密码),这就是答案.虽然即使这样足以保证有其他地方,你是不是`OpenSSL_cleanse`ing? (3认同)
  • @JonathanWakely:嗯,我想我们都同意你可能不应该使用`std :: string`来处理敏感数据:) (2认同)

Dav*_*mas 13

std :: string是存储机密的不良选择.由于字符串是可复制的,有时副本不被注意,你的秘密可能"得到腿".此外,字符串扩展技术可能会导致您的秘密片段(或全部)的多个副本.

经验决定了一个可移动的,不可复制的,擦拭干净的摧毁,非智能(没有棘手的副本)类.

  • 这是"擦上消灭干净"的子问题是OP正在设法解决. (4认同)
  • 我断言,析构函数是用于擦拭的最佳场所.责任在于类设计师,而不是每个使用它的实例 (3认同)
  • @DavidThomas考虑这个问题:'SecretHandler(的std :: string &&无保护);` - 你将如何放弃了`unprotected`输入?(这是一个问题是关于什么的) (3认同)
  • @hauron挑战一个问题的假设是一个完全合法的答案."答案可能是'不这样做’,但它也应该包括'试试这个,而不是’." 参见[我怎样写一个很好的答案?](http://stackoverflow.com/help/how-to-answer).问题提供者有责任明确他们必须在哪些限制下运作,如果做某事是个坏主意,那么说明为什么你无法摆脱它是很重要和必要的. (3认同)

Dav*_*aim 12

您可以使用std::fill垃圾填充字符串:

std::fill(str.begin(),str.end(), 0);
Run Code Online (Sandbox Code Playgroud)

请注意,简单地清除或缩小字符串(使用clear或等方法shrink_to_fit)并不能保证字符串数据将从进程内存中删除.恶意进程可能会转储进程内存,如果字符串未被正确覆盖,则可以提取秘密.

奖励:有趣的是,出于安全原因而丢弃字符串数据的能力迫使某些编程语言(如Java)返回密码char[]而不是String.在Java中,String是不可变的,因此"废弃"它将创建一个新的字符串副本.因此,您需要一个可修改的对象,例如char[]不使用copy-on-write.

编辑:如果您的编译器确实优化了此调用,您可以使用特定的编译器标志来确保不会优化废弃函数:

#ifdef WIN32

#pragma optimize("",off)
void trashString(std::string& str){
   std::fill(str.begin(),str.end(),0);
}
#pragma optimize("",on)

#endif

#ifdef __GCC__

void __attribute__((optimize("O0"))) trashString(std::string& str) {
       std::fill(str.begin(),str.end(),0);
}


#endif

#ifdef __clang__

void __attribute__ ((optnone))  trashString(std::string& str) {
       std::fill(str.begin(),str.end(),0);
}

#endif
Run Code Online (Sandbox Code Playgroud)

  • 我听说,像'的std :: fill`或`"休闲"方式memset`可以通过编译器进行优化.如果编译器看到的字符串没有后续的使用,也不会产生多余的数据变化.像`OpenSSL_cleanse`功能旨在防止它.我对吗? (10认同)
  • MSVC的确切原因,`memset`可以通过今天的优化编译器被优化掉创建`memset_s`.它越来越难垃圾桶一个不再使用的对象作为编译器的存储器/接头将其视为不需要的. (6认同)
  • 为什么你有一个很难相信,编译器会优化掉,不会有在C++对象模型理解的方式观察到的副作用的代码? (5认同)
  • 您可能需要给予一定的考虑,以保证标准的*不*提供:您可以根据自己组装的字符串,它可能是它的某些部分是在内存中留下的其他地方,如果的std :: string必须做一个重新定位,或如果副本是在调用堆栈中的某个地方制作的. (3认同)
  • @ilotXXI这是真的.OpenSSL_cleanse似乎永远不会被优化掉. (2认同)