安全清除内存并重新分配

时间:2012-05-21 10:57:40

标签: c++ security memory cryptography passwords

在讨论here之后,如果您想要一个用于在内存中存储敏感信息(例如密码)的安全类,您必须:

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

所以这听起来不错,我创建了一个测试类来查看它是否有效。所以我做了一个简单的测试用例,我继续添加单词“LOL”和“WUT”,然后在这个安全缓冲类中加上一个数字大约一千次,销毁该对象,然后再做一些导致核心转储的事情。 / p>

由于该类应该在破坏之前安全地清除内存,所以我不应该在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附加引起的重新分配,应用程序的整个内存区域是否重新分配?

此外,This is SecByteBlock's Documentation

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

edit2 :更新代码,以便更容易编译,以及编译说明

final edit3 :看起来memcpy是罪魁祸首。请参阅下面的答案中的Rasmus'mymemcpy实现。

5 个答案:

答案 0 :(得分:23)

尽管在coredump中出现,但密码实际上并不在内存中 清除缓冲区后再进行清理。问题在于memcpy 足够长的字符串将密码泄漏到SSE寄存器中,那些 是什么出现在coredump。

size的{​​{1}}参数大于某个时 阈值 - 80 bytes on the mac - 然后使用SSE指令来执行此操作 记忆复制。这些说明更快,因为它们可以复制16 一次一个字节并行而不是逐个字符, 逐字节或逐字逐句。这是源代码的关键部分 Libc on the mac

memcpy

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 s 源地址寄存器, %rsi d 登记地址寄存器。每次绕圈, 64个字节从源缓冲区复制到4个16字节SSE寄存器 %rdi;然后将这些寄存器中的值复制到 目的地缓冲区。

源文件中有很多东西可以确保副本出现 仅在对齐的地址上,填写剩余副本的部分 在做了64字节的块之后,并处理源和的情况 目的地重叠。

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

这是对Rasmus计划的修改,显示了这一点:

xmm{0,1,2,3}

在我的Mac上,打印出来:

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

现在,检查核心转储,密码只发生一次, 和那个确切的0\0LOLWUT130\0LOLWUT140\0LOLWUT150\0LOLWUT160\0LOLWUT170\0LOLWUT180\0\0\0 字符串一样。核心转储必须 包含所有寄存器的副本,这就是为什么该字符串存在的原因 - 它就是 0\0LOLWUT130\0...180\0\0\0个寄存器的值。

所以调用后密码实际上不在RAM中 xmm{0,1,2,4},它似乎只是,因为它实际上在某些内容中 仅出现在coredump中的寄存器。如果你担心的话 SecureWipeBuffer有一个漏洞可以被RAM冻结利用, 不再担心。如果在寄存器中有密码副本困扰你, 使用不使用SSE2寄存器的修改后的memcpy,或清除它们 当它完成。如果你对此真的很偏执,继续测试你的 coredumps以确保编译器没有优化你的 密码清除代码。

答案 1 :(得分: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;    
}

如果用memcpy替换mymemcpy或使用较小的尺寸,问题就会消失,所以我最好的猜测是内置的memcpy会将部分复制的数据留在内存中。

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

答案 2 :(得分:2)

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

这个其他的SO问题在解释它方面做得不错: Is a string literal in c++ created in static memory?

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

答案 3 :(得分:2)

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

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

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

答案 4 :(得分:0)

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