Borrar memoria de forma segura y reasignaciones

Siguiendo la discusión aquí, si desea tener una clase segura para almacenar información confidencial (por ejemplo, contraseñas) en la memoria, debe:

  • memset/borrar la memoria antes de liberarla
  • las reasignaciones también deben seguir la misma regla: en lugar de usar realloc, use malloc para crear una nueva región de memoria, copie la anterior a la nueva y luego configure/borre la memoria anterior antes de liberarla finalmente

Esto suena bien y creé una clase de prueba para ver si funciona. Así que hice un caso de prueba simple en el que sigo agregando las palabras "LOL" y "WUT", seguidas de un número a esta clase de búfer seguro unas mil veces, destruyendo ese objeto, antes de finalmente hacer algo que provoque un volcado del núcleo.

Dado que se supone que la clase debe borrar de forma segura la memoria antes de la destrucción, se supone que no puedo encontrar un "LOLWUT" en el volcado del núcleo. Sin embargo, me las arreglé para encontrarlos todavía, y me pregunté si mi implementación tiene errores. Sin embargo, probé lo mismo usando SecByteBlock de la biblioteca CryptoPP:

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

Y luego compilar usando:

g++ clearer.cpp -lcryptopp -O0

Y luego habilite el volcado del núcleo

ulimit -c 99999999

Pero luego, habilitar el volcado del núcleo y ejecutarlo

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

da la siguiente salida

Segmentation fault (core dumped)
Binary file core matches
hello

¿Qué está causando esto? ¿Se reasignó toda la región de memoria de la aplicación debido a la reasignación provocada por la adición de SecByteBlock?

También, trabaja para Esta es la documentación de SecByteBlock

editar: Después de verificar el volcado del núcleo usando vim, obtuve esto: http://imgur.com/owkaw

edit2: código actualizado para que sea más fácil de compilar e instrucciones de compilación

edición final3: Parece que memcpy es el culpable. Ver Rasmus' mymemcpy implementación en su respuesta a continuación.

preguntado el 21 de mayo de 12 a las 11:05

No creo que sea la razón de lo que estás viendo, pero sí sabes que llamar memset en alguna memoria no impide que todavía haya una copia en algún archivo de intercambio en alguna parte? Y en general, el resultado de la memset No es necesario penetrar en todas las capas de caché, pero menciono el archivo de intercambio porque es el lugar más persistente y, por lo tanto, el más descaradamente peligroso para los datos confidenciales. -

@SteveJessop hmm, probablemente sea por eso que Windows tiene SecureZeroMemory o algo así. Me pregunto cuál sería el equivalente de linux/posix... si lo hay. -

Un optimizador suficientemente agresivo podría eliminar memset a bloques que nunca se vuelven a leer. SecureZeroMemory está ahí por una razón! -

¡Mi reino para una versión posix/linux/iOS de securezeromemory entonces! -

@kamziro: Estoy bastante seguro de que tanto Linux como iOS siempre borrarán su memoria antes de permitir que otro proceso la vea. Si no es así, es porque desactivó algo al compilar el núcleo :-) En última instancia, lo que está peleando es la duración de la ventana dentro de la cual un atacante podría ver el contenido de la memoria provocando y leyendo un volcado del núcleo, inspeccionando directamente la memoria RAM, etc. La ventana existe o no existe sin importar lo que haga, ya que normalmente no hay razón para que un atacante pueda hacer eso. después de lo borras pero no antes. -

5 Respuestas

A pesar de aparecer en el volcado del núcleo, la contraseña ya no está en la memoria después de borrar los búferes. El problema es ese memcpying una cadena suficientemente larga filtra la contraseña en los registros SSE, y aquellos son los que aparecen en el volcado del núcleo.

Cuando el size argumento para memcpy es mayor que cierto umbral—80 bytes en mac—luego se usan instrucciones SSE para hacer la copia de memoria. Estas instrucciones son más rápidas porque pueden copiar 16 bytes a la vez en paralelo en lugar de ir carácter por carácter, byte por byte o palabra por palabra. Aquí está la parte clave del código fuente de libc en el 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 es el registro de índice de bucle, %rsi es la sregistro de direcciones de origen, y %rdi es la dregistro de direcciones de destino. Cada vez que se ejecuta el bucle, se copian 64 bytes del búfer de origen a los 4 registros SSE de 16 bytes xmm{0,1,2,3}; luego, los valores de esos registros se copian en el búfer de destino.

Hay muchas más cosas en ese archivo de origen para asegurarse de que las copias se produzcan solo en direcciones alineadas, para completar la parte de la copia que sobra después de hacer fragmentos de 64 bytes y para manejar el caso en el que el origen y el destino se superponen.

Sin emabargo-¡Los registros SSE no se borran después de su uso! Eso significa que 64 bytes del búfer que se copió todavía están presentes en el xmm{0,1,2,3} registros.

Aquí hay una modificación del programa de Rasmus que muestra esto:

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

En mi mac, esto imprime:

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

Ahora, al examinar el volcado del núcleo, la contraseña solo aparece una sola vez, y como exactamente 0\0LOLWUT130\0...180\0\0\0 cuerda. El volcado del núcleo debe contener una copia de todos los registros, razón por la cual esa cadena está allí: son los valores del xmm{0,1,2,4} registros.

So la contraseña ya no está en la RAM después de llamar SecureWipeBuffer, solo parece ser porque en realidad está en algunos registros que solo aparecen en el volcado. Si estás preocupado por memcpy teniendo una vulnerabilidad que podría ser aprovechada por la congelación de RAM, no se preocupe más. Si le molesta tener una copia de la contraseña en los registros, utilice una modificada memcpy que no usa los registros SSE2, o los borra cuando termina. Y si está realmente paranoico acerca de esto, siga probando sus volcados de núcleo para asegurarse de que el compilador no esté optimizando su código de limpieza de contraseñas.

Respondido el 29 de enero de 13 a las 22:01

Me enfrento a un problema similar cuando trato con entornos seguros/de confianza. Sabía que algunas optimizaciones puede de hecho, limpie el conjunto de memoria. Por lo que vale, aquí hay otra solución del Intel SGX SDK que usa un puntero volátil para una función de conjunto de memoria personalizada: github.com/intel/linux-sgx/blob/… - X99

Aquí hay otro programa que reproduce el problema más directamente:

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

Si reemplazas memcpy con mymemcpy o use tamaños más pequeños, el problema desaparece, por lo que mi mejor suposición es que el memcpy incorporado hace algo que deja parte de los datos copiados en la memoria.

Supongo que esto solo muestra que borrar datos confidenciales de la memoria es prácticamente imposible a menos que esté diseñado en todo el sistema desde cero.

contestado el 22 de mayo de 12 a las 09:05

Maldiciones, eso es desafortunado. Supongo que eso descarta comercializar mi aplicación con "¡Seguridad contra análisis forense de RAM!" - Kamziro

En realidad, su mymemcpy parece haber resuelto el problema. ¡Parece que el estándar realmente fue lo que causó la dispersión de la memoria en la naturaleza! - Kamziro

@kamziro. Pero, por extraño que parezca, usar -fno-builtin-memcpy no funciona. - Rasmus faber

@kamziro: he creado un boleto en el rastreador de errores de Crypto ++ por si acaso. - Rasmus faber

Los literales de cadena se almacenarán en la memoria y no serán administrados por la clase SecByteBlock.

Esta otra pregunta SO hace un trabajo decente al explicarlo: ¿Se crea una cadena literal en c ++ en la memoria estática?

Puede probar y confirmar si los literales de cadena pueden tener en cuenta las coincidencias de grep al ver cuántas coincidencias obtiene. También puede imprimir las ubicaciones de memoria de los búferes SecByteBlock e intentar ver si se corresponden con las ubicaciones en el volcado del núcleo que coinciden con su marcador.

contestado el 23 de mayo de 17 a las 12:05

La coincidencia de la memoria de volcado del núcleo se parece a esto: "@^@^@^@^@^@^@^@^@^@^@^@^@0LOLWUT231LOLWUTOLWUT229LOLWUT23216LOLWUT217LOLWUT218LOLWUT219LOLWUT220LOLWUT221LOLWUT222LOLWUT223LOLWUT224LOL^^@225LOL^^@LOLWUT@226LOL^@XNUMXLOL^@XNUMX @^@^" (de vim), por lo que definitivamente son del bloque secbyte. Establecer el ciclo en cero elementos no da coincidencias. La configuración a cero también se realiza utilizando la entrada del usuario, por lo que definitivamente tampoco está "optimizado por el compilador": Kamziro

Sin inspeccionar los detalles de memcpy_s, sospecho que lo que está viendo es un búfer de pila temporal utilizado por memcpy_s para copiar pequeños búferes de memoria. Puede verificar esto ejecutando un depurador y viendo si LOLWUT aparece al ver la memoria de pila.

[La implementación de reallocate en usos de Crypto++ memcpy_s al cambiar el tamaño de las asignaciones de memoria, por lo que podría encontrar una cantidad de LOLWUT cadenas en la memoria. Además, el hecho de que muchos diferentes LOLWUT las cadenas superpuestas en ese volcado sugieren que es un búfer temporal que se está reutilizando.]

La versión personalizada de memcpy eso es solo un bucle simple que no requiere almacenamiento temporal más allá de los mostradores, por lo que sin duda sería más seguro que cómo memcpy_s está implementado.

Respondido el 29 de enero de 13 a las 00:01

+1. memset_s es la única forma que cumple con las especificaciones para borrar los búferes de forma segura. También implica que antes de C++ 11, no había forma de borrar los búfer de forma segura (es decir, mientras se ajustaba a las especificaciones). - ishaq

Yo sugeriría que la forma de hacerlo es cifrar los datos en la memoria. De esa manera, los datos siempre están seguros, ya sea que todavía estén en la memoria o no. El inconveniente, por supuesto, es una sobrecarga en términos de cifrar/descifrar los datos cada vez que se accede a ellos.

Respondido el 29 de enero de 13 a las 14:01

El compilador lo suficientemente "inteligente" para optimizar memset en la memoria estás a punto de free, también podría eliminar el último cifrado. O la próxima versión del compilador podría hacerlo. - bo persona

Le sugiero que solo cifre/descifre la sección específica que está leyendo/escribiendo actualmente. - Jack Aidley

No es la respuesta que estás buscando? Examinar otras preguntas etiquetadas or haz tu propia pregunta.