25

按照这里的讨论,如果你想有一个安全的类来在内存上存储敏感信息(例如密码),你必须:

  • memset/在释放内存之前清除内存
  • 重新分配也必须遵循相同的规则——不要使用 realloc,而是使用 malloc 创建一个新的内存区域,将旧内存复制到新内存,然后在最终释放之前 memset/clear 旧内存

所以这听起来不错,我创建了一个测试类来看看这是否有效。所以我做了一个简单的测试用例,我不断添加单词“LOL”和“WUT”,然后在这个安全缓冲区类中添加一个数字大约一千次,销毁该对象,然后最终执行导致核心转储的操作。

由于该类应该在销毁之前安全地清除内存,因此我不应该能够在 coredump 上找到“LOLWUT”。但是,我还是设法找到了它们,并想知道我的实现是否只是错误。但是,我使用 CryptoPP 库的 SecByteBlock 尝试了同样的事情:

#include <cryptopp/osrng.h>
#include <cryptopp/dh.h>
#include <cryptopp/sha.h>
#include <cryptopp/aes.h>
#include <cryptopp/modes.h>
#include <cryptopp/filters.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
using namespace std;

int main(){
   {
      CryptoPP::SecByteBlock moo;

      int i;
      for(i = 0; i < 234; i++){
         moo += (CryptoPP::SecByteBlock((byte*)"LOL", 3));
         moo += (CryptoPP::SecByteBlock((byte*)"WUT", 3));

         char buffer[33];
         sprintf(buffer, "%d", i);
         string thenumber (buffer);

         moo += (CryptoPP::SecByteBlock((byte*)thenumber.c_str(), thenumber.size()));
      }

      moo.CleanNew(0);

   }

   sleep(1);

   *((int*)NULL) = 1;

   return 0;
}

然后编译使用:

g++ clearer.cpp -lcryptopp -O0

然后启用核心转储

ulimit -c 99999999

但是,启用核心转储并运行它

./a.out ; grep LOLWUT core ; echo hello

给出以下输出

Segmentation fault (core dumped)
Binary file core matches
hello

这是什么原因造成的?由于 SecByteBlock 的追加引起的重新分配,应用程序的整个内存区域是否重新分配了?

另外,这是 SecByteBlock 的文档

编辑:使用 vim 检查核心转储后,我得到了这个:http: //imgur.com/owkaw

edit2:更新了代码,使其更容易编译,以及编译说明

最终编辑3:看起来 memcpy 是罪魁祸首。请参阅 Rasmusmymemcpy在下面的回答中的实施。

4

5 回答 5

25

尽管出现在 coredump 中,但在清除缓冲区后,密码实际上不再存在于内存中。问题是memcpy足够长的字符串会将密码泄漏到 SSE 寄存器中,而这些 正是 coredump 中显示的内容。

sizeto 的参数memcpy大于某个阈值时——<a href="http://opensource.apple.com/source/Libc/Libc-825.25/x86_64/string/bcopy_sse42.s">mac 上的 80 字节——那么SSE 指令用于进行内存复制。这些指令速度更快,因为它们可以一次并行复制 16 个字节,而不是逐个字符、逐字节或逐字地复制。以下是 Mac 上 Libc源代码的关键部分 :

LAlignedLoop:               // loop over 64-byte chunks
    movdqa  (%rsi,%rcx),%xmm0
    movdqa  16(%rsi,%rcx),%xmm1
    movdqa  32(%rsi,%rcx),%xmm2
    movdqa  48(%rsi,%rcx),%xmm3

    movdqa  %xmm0,(%rdi,%rcx)
    movdqa  %xmm1,16(%rdi,%rcx)
    movdqa  %xmm2,32(%rdi,%rcx)
    movdqa  %xmm3,48(%rdi,%rcx)

    addq    $64,%rcx
    jnz     LAlignedLoop

    jmp     LShort                  // copy remaining 0..63 bytes and done

%rcx是循环索引寄存器,是%rsi源地址寄存器,目标地址寄存器。每次循环运行,64 个字节从源缓冲区复制到 4 个 16 字节 SSE 寄存器 ;然后将这些寄存器中的值复制到目标缓冲区。%rdixmm{0,1,2,3}

该源文件中有更多内容,以确保副本仅发生在对齐的地址上,以填充在执行 64 字节块后剩余的副本部分,并处理源和目标重叠的情况。

但是—<strong>使用后不会清除 SSE 寄存器!这意味着被复制的 64 字节缓冲区仍然存在于xmm{0,1,2,3}寄存器中。

这是对 Rasmus 程序的修改,它显示了这一点:

#include <ctype.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <emmintrin.h>

inline void SecureWipeBuffer(char* buf, size_t n){
  volatile char* p = buf;
  asm volatile("rep stosb" : "+c"(n), "+D"(p) : "a"(0) : "memory");
}

int main(){
  const size_t size1 = 200;
  const size_t size2 = 400;

  char* b = new char[size1];
  for(int j=0;j<size1-10;j+=10){
    memcpy(b+j, "LOL", 3);
    memcpy(b+j+3, "WUT", 3);
    sprintf((char*) (b+j+6), "%d", j);
  }
  char* nb = new char[size2];
  memcpy(nb, b, size1);
  SecureWipeBuffer(b,size1);
  SecureWipeBuffer(nb,size2);

  /* Password is now in SSE registers used by memcpy() */
  union {
    __m128i a[4];
    char c;
  };
  asm ("MOVDQA %%xmm0, %0": "=x"(a[0]));
  asm ("MOVDQA %%xmm1, %0": "=x"(a[1]));
  asm ("MOVDQA %%xmm2, %0": "=x"(a[2]));
  asm ("MOVDQA %%xmm3, %0": "=x"(a[3]));
  for (int i = 0; i < 64; i++) {
      char p = *(&c + i);
      if (isprint(p)) {
        putchar(p);
      } else {
          printf("\\%x", p);
      }
  }
  putchar('\n');

  return 0;
}

在我的 Mac 上,打印:

0\0LOLWUT130\0LOLWUT140\0LOLWUT150\0LOLWUT160\0LOLWUT170\0LOLWUT180\0\0\0

现在,检查核心转储,密码只出现一次,并且是那个确切的0\0LOLWUT130\0...180\0\0\0字符串。核心转储必须包含所有寄存器的副本,这就是该字符串存在的原因——它是xmm{0,1,2,4}寄存器的值。

因此,在调用 之后,密码实际上不再在 RAM 中 SecureWipeBuffer,这只是因为它实际上是在某些只出现在 coredump 中的寄存器中。如果您担心 memcpy存在可能被 RAM 冻结利用的漏洞,请不要再担心。如果在寄存器中有密码副本困扰您,请使用memcpy不使用 SSE2 寄存器的修改,或在完成后清除它们。如果您对此真的很偏执,请继续测试您的核心转储,以确保编译器不会优化您的密码清除代码。

于 2013-01-29T20:48:46.800 回答
10

这是另一个更直接地重现问题的程序:

#include <stdlib.h>
#include <stdio.h>
#include <string.h>

inline void SecureWipeBuffer(char* buf, size_t n){
  volatile char* p = buf;
  asm volatile("rep stosb" : "+c"(n), "+D"(p) : "a"(0) : "memory");
}

void mymemcpy(char* b, const char* a, size_t n){
  char* s1 = b;
  const char* s2= a;
  for(; 0<n; --n) *s1++ = *s2++;
}

int main(){
  const size_t size1 = 200;
  const size_t size2 = 400;

  char* b = new char[size1];
  for(int j=0;j<size1-10;j+=10){
    memcpy(b+j, "LOL", 3);
    memcpy(b+j+3, "WUT", 3);
    sprintf((char*) (b+j+6), "%d", j);
  }
  char* nb = new char[size2];
  memcpy(nb, b, size1);
  //mymemcpy(nb, b, size1);
  SecureWipeBuffer(b,size1);
  SecureWipeBuffer(nb,size2);

  *((int*)NULL) = 1;

  return 0;    
}

如果你用更小的尺寸替换memcpymymemcpy使用更小的尺寸,问题就会消失,所以我最好的猜测是内置的 memcpy 做了一些事情,将部分复制的数据留在内存中。

我想这只是表明从内存中清除敏感数据实际上是不可能的,除非它是从头开始设计到整个系统中的。

于 2012-05-22T07:53:25.130 回答
2

字符串文字将存储在内存中,不由 SecByteBlock 类管理。

另一个SO问题很好地解释了它: C++中的字符串文字是在静态内存中创建的吗?

您可以通过查看获得的匹配数来尝试确认字符串文字是否可以解释 grep 匹配。您还可以打印出 SecByteBlock 缓冲区的内存位置,并尝试查看它们是否与核心转储中与您的标记匹配的位置相对应。

于 2012-05-22T02:43:33.810 回答
2

在不检查 的详细信息的情况下memcpy_s,我怀疑您看到的是一个临时堆栈缓冲区,用于memcpy_s复制小内存缓冲区。您可以通过在调试器中运行并LOLWUT查看查看堆栈内存时是否显示来验证这一点。

[在调整内存分配大小时reallocate使用 Crypto++ 中的实现memcpy_s,这就是为什么您能够LOLWUT在内存中找到一些字符串的原因。此外,该转储中许多不同的LOLWUT字符串重叠的事实表明它是一个正在被重用的临时缓冲区。]

它的自定义版本memcpy只是一个简单的循环,不需要计数器之外的临时存储,因此这肯定比memcpy_s实现方式更安全。

于 2013-01-29T00:14:41.723 回答
0

我建议这样做的方法是加密内存中的数据。这样,无论数据是否仍在内存中,数据始终是安全的。当然,缺点是每次访问数据时加密/解密数据的开销。

于 2013-01-29T14:12:10.413 回答