Подтвердить что ты не робот

Безопасная очистка памяти и перераспределение

После обсуждения здесь, если вы хотите иметь безопасный класс для хранения конфиденциальной информации (например, паролей) в памяти, вам необходимо:

  • memset/очистить память перед ее освобождением.
  • reallocations также должны следовать тому же правилу - вместо использования realloc, используйте malloc для создания новой области памяти, скопируйте старый в новый, а затем memset/очистите старую память, прежде чем освободить ее.

Итак, это звучит неплохо, и я создал тестовый класс, чтобы увидеть, работает ли это. Поэтому я сделал простой тестовый сценарий, в котором я продолжаю добавлять слова "LOL" и "WUT", а затем номер в этот класс защищенного буфера около тысячи раз, уничтожая этот объект, прежде чем, наконец, сделать что-то, что вызывает основной дамп.

Поскольку класс должен надежно очистить память до разрушения, я не должен быть в состоянии найти "LOLWUT" на coredump. Тем не менее, мне удалось найти их еще и подумал, не работает ли моя реализация. Тем не менее, я попробовал то же самое, используя библиотеку 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

Что вызывает это? Изменилась ли вся область памяти для приложения realloc сама из-за перераспределения, вызванного добавлением SecByteBlock?

Кроме того, Это документация SecByteBlock

изменить. После проверки дампа ядра с помощью vim я получил следующее: http://imgur.com/owkaw

edit2: обновленный код, чтобы он был более компилируемым и команды компиляции

final edit3. Похоже, что memcpy является виновником. См. Реализацию Rasmus mymemcpy в его ответе ниже.

4b9b3361

Ответ 1

Вот еще одна программа, которая напрямую воспроизводит проблему:

#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

Несмотря на то, что отображается в coredump, пароль фактически не находится в памяти больше после очистки буферов. Проблема состоит в том, что memcpy ing достаточно длинная строка утечки пароля в регистры SSE, и те это то, что появляется в coredump.

Когда аргумент size для memcpy больше определенного threshold - 80 байт на mac - тогда инструкции SSE используются для выполнения копирование памяти. Эти инструкции быстрее, потому что они могут копировать 16 байты одновременно параллельно, а не идти по-символу, байт по-байтам или слово за словом. Здесь ключевая часть исходного кода Libc на mac:

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 - это регистр адресов s ource, и %rdi является регистром адресов адресации d. Каждый бегает вокруг цикла, 64 байта копируются из исходного буфера в 4 16-байтных регистра SSE xmm{0,1,2,3}; то значения в этих регистрах копируются в целевого буфера.

В этом исходном файле больше материала, чтобы убедиться, что копии только по выровненным адресам, чтобы заполнить часть оставшейся копии после выполнения 64-байтовых кусков и для обработки случая, когда источник и перекрытие пункта назначения.

Однако регистры SSE не очищаются после использования! Это означает, что 64 байта буфера, который был скопирован, все еще присутствует в регистрах xmm{0,1,2,3}.

Здесь есть модификация программы Расмуса, которая показывает это:

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

Итак, пароль уже не находится в ОЗУ после вызова SecureWipeBuffer, он выглядит только как, потому что он фактически находится в некоторых регистры, которые появляются только в coredump. Если вас беспокоит memcpy с уязвимостью, которая может быть использована при замораживании RAM, больше не беспокойтесь. Если вам нужна копия пароля в реестрах, используйте модифицированный memcpy, который не использует регистры SSE2, или очищает их когда это будет сделано. И если вы действительно параноики об этом, продолжайте проверять свои coredumps, чтобы убедиться, что компилятор не оптимизирует ваш код очистки пароля.

Ответ 3

Строковые литералы будут храниться в памяти и не управляться классом SecByteBlock.

Этот другой вопрос SO делает достойную работу, объясняя это: Является строковым литералом в С++, созданным в статической памяти?

Вы можете попробовать и проверить, могут ли совпадения grep обрабатываться строковыми литералами, видя, сколько совпадений вы получаете. Вы также можете распечатать ячейки памяти буферов SecByteBlock и попытаться выяснить, соответствуют ли они местоположениям в дампе ядра, которые соответствуют вашему маркеру.

Ответ 4

Не проверяя детали memcpy_s, я подозреваю, что то, что вы видите, это временный буфер стека, используемый memcpy_s для копирования небольших буферов памяти. Вы можете проверить это, запустив в отладчике и посмотрев, появляется ли LOLWUT при просмотре памяти стека.

[Реализация reallocate в Crypto ++ использует memcpy_s при изменении размеров распределений памяти, поэтому вы можете найти некоторое количество строк LOLWUT в памяти. Кроме того, тот факт, что многие разные строки LOLWUT перекрываются в этом дампе, позволяют предположить, что это временный буфер, который используется повторно.]

Пользовательская версия memcpy, которая представляет собой простой цикл, не требует временного хранения за пределами счетчиков, поэтому это будет, безусловно, более безопасным, чем реализация memcpy_s.

Ответ 5

Я бы предположил, что способ сделать это - зашифровать данные в памяти. Таким образом, данные всегда безопасны независимо от того, находится ли она в памяти или нет. Недостатком, конечно же, является накладные расходы с точки зрения шифрования/дешифрования данных каждый раз, когда к нему обращаются.