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

How to cleanse (overwrite with random bytes) std::string internal buffer?

本文关键字:std string 内部 缓冲区 覆盖 何清理 随机 字节      更新时间:2023-10-16

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

CCD_ 2提供函数CCD_。

现在,由于内存是连续的,由于作用域结束,变量将在清理后立即销毁,因此:是否安全

char* modifiable = const_cast<char*>(secretString.data());
OpenSSL_cleanse(modifiable, secretString.size());

根据此处引用的标准:

$5.2.11/7-注意:根据对象的类型,通过指针、左值或指向数据成员的指针的写操作可能会产生未定义的行为(7.1.5.1),该指针是由抛出const-qualifier68const_cast产生的

这会建议其他情况,但上述条件(持续的,只是被删除)是否安全?

标准明确规定您不能写入data()返回的const char*,所以不要这样做。

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

if (secretString.size())
  OpenSSL_cleanse(&secretString.front(), secretString.size());

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

if (secretString.capacity()) {
  secretString.resize(secretString.capacity());
  OpenSSL_cleanse(&secretString.front(), secretString.size());
}

它可能是安全的。但不能保证。

但是,由于C++11std::string必须实现为连续数据,这样您就可以使用其第一个元素&secretString[0]的地址安全地访问其内部数组。

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

std::string是存储机密的糟糕选择。由于字符串是可复制的,而且有时副本会被忽视,所以你的秘密可能会"搁浅"。此外,字符串扩展技术可能会导致您的秘密片段(或全部)的多个副本。

经验决定了一个可移动的、不可复制的、销毁时擦干净的、不聪明的(没有棘手的副本)类。

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

std::fill(str.begin(),str.end(), 0);

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

好处:有趣的是,出于安全原因丢弃字符串数据的能力迫使Java等一些编程语言将密码返回为char[],而不是String。在Java中,String是不可变的,因此"破坏"它将生成字符串的新副本。因此,您需要一个可修改的对象,如char[],它不使用写时复制。

编辑:如果您的编译器优化了这个调用,您可以使用特定的编译器标志来确保垃圾函数不会被优化:

#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

有一个更好的答案:不要

CCD_ 18是一个设计为用户友好和高效的类。它在设计时没有考虑到密码学,所以几乎没有什么保证可以帮助你。例如,不能保证您的数据没有被复制到其他地方。充其量,您可以希望特定编译器的实现为您提供所需的行为。

如果你真的想把一个秘密当作一个秘密来处理,你应该使用专为处理秘密而设计的工具来处理它。事实上,您应该为攻击者的能力开发一个威胁模型,并相应地选择您的工具。

在CentOS 6、Debian 8和Ubuntu 16.04上测试的解决方案(g++/crang++、O0、O1、O2、O3):

secretString.resize(secretString.capacity(), '');
OPENSSL_cleanse(&secretString[0], secretString.size());
secretString.clear();

如果你真的很偏执,你可以将清理后的字符串中的数据随机化,这样就不会泄露字符串的长度或包含敏感数据的位置:

#include <string>
#include <stdlib.h>
#include <string.h>
typedef void* (*memset_t)(void*, int, size_t);
static volatile memset_t memset_func = memset;
void cleanse(std::string& to_cleanse) {
  to_cleanse.resize(to_cleanse.capacity(), '');
  for (int i = 0; i < to_cleanse.size(); ++i) {
    memset_func(&to_cleanse[i], rand(), 1);
  }
  to_cleanse.clear();
}

如果你想的话,你也可以给rand()种子。

您也可以在不依赖openssl的情况下进行类似的字符串清理,方法是使用explicit_bzero将内容置空:

#include <string>
#include <string.h>
int main() {
  std::string secretString = "ajaja";
  secretString.resize(secretString.capacity(), '');
  explicit_bzero(&secretString[0], secretString.size());
  secretString.clear();
  return 0;
}