安全清除内存并重新分配

kam*_*iro 23 c++ memory security passwords cryptography

这里讨论之后,如果你想要一个安全的类来存储内存中的敏感信息(例如密码),你必须:

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

所以这听起来不错,我创建了一个测试类来查看它是否有效.所以我做了一个简单的测试用例,我继续添加单词"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;
}
Run Code Online (Sandbox Code Playgroud)

然后编译使用:

g++ clearer.cpp -lcryptopp -O0
Run Code Online (Sandbox Code Playgroud)

然后启用核心转储

ulimit -c 99999999
Run Code Online (Sandbox Code Playgroud)

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

./a.out ; grep LOLWUT core ; echo hello
Run Code Online (Sandbox Code Playgroud)

给出以下输出

Segmentation fault (core dumped)
Binary file core matches
hello
Run Code Online (Sandbox Code Playgroud)

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

另外,这是SecByteBlock的文档

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

edit2:更新的代码,因此它更易于编译,并且编译指令

最终编辑3:看起来memcpy是罪魁祸首.请参阅mymemcpy下面的答案,了解Rasmus的实施情况.

and*_*otn 23

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

size参数to memcpy大于某个阈值-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
Run Code Online (Sandbox Code Playgroud)

%rcx是循环索引寄存器,%rsi小号乌尔斯河地址寄存器,并且%rdid estination地址寄存器.每次循环运行,64个字节从源缓冲区复制到4个16字节SSE寄存器 xmm{0,1,2,3}; 然后将这些寄存器中的值复制到目标缓冲区.

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

但是 - 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;
}
Run Code Online (Sandbox Code Playgroud)

在我的Mac上,这打印:

0\0LOLWUT130\0LOLWUT140\0LOLWUT150\0LOLWUT160\0LOLWUT170\0LOLWUT180\0\0\0
Run Code Online (Sandbox Code Playgroud)

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

因此,调用后密码实际上不在RAM中 SecureWipeBuffer,它似乎只是因为它实际上在某些寄存器中才出现在coredump中.如果你担心 memcpy有一个可以被RAM冻结利用的漏洞,那就不用再担心了.如果在寄存器中有密码副本困扰您,请使用memcpy不使用SSE2寄存器的修改,或在完成后清除它们.如果你对此非常偏执,请继续测试你的coredumps,以确保编译器没有优化你的密码清除代码.


Ras*_*ber 9

这是另一个程序,可以更直接地再现问题:

#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;    
}
Run Code Online (Sandbox Code Playgroud)

如果更换memcpymymemcpy或使用较小尺寸的问题消失了,所以我最好的猜测是,内置的memcpy做一些事情,离开在内存中拷贝数据的一部分.

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

  • @kamziro:我已经在Crypto ++的bugtracker中创建了[门票](https://sourceforge.net/apps/trac/cryptopp/ticket/18). (2认同)