2012-05-21 16 views
21

Tras el debate here, si usted quiere tener una clase segura para almacenar información sensible (por ejemplo, contraseñas) en la memoria, usted tiene que:Borrado de la memoria de forma segura y reasignaciones

  • memset/cancelar la memoria antes liberándola
  • reasignaciones también deben seguir la misma regla - en lugar de utilizar realloc, usar malloc para crear una nueva región de memoria, copie el antiguo al nuevo, y luego memset/borrar la memoria antigua antes de liberar finalmente

Así que esto funciona bien, y creé una clase de prueba para ver si esto funciona. Así que hice un caso de prueba simple donde sigo agregando las palabras "LOL" y "WUT", seguidas de un número para esta clase de buffer seguro alrededor de mil veces, destruyendo ese objeto, antes de finalmente hacer algo que causa un volcado del núcleo.

Como se supone que la clase limpia la memoria de forma segura antes de la destrucción, se supone que no puedo encontrar un "LOLWUT" en el núcleo. Sin embargo, logré encontrarlos todavía, y me pregunté si mi implementación es incorrecta. Sin embargo, he intentado lo mismo usando SecByteBlock de CryptoPP biblioteca:

#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 a continuación, compilar usando:

g++ clearer.cpp -lcryptopp -O0 

Y a continuación, active de vaciado de

ulimit -c 99999999 

Pero entonces, lo que permite volcado de memoria y ejecutándolo

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

da la siguiente salida

Segmentation fault (core dumped) 
Binary file core matches 
hello 

¿Qué está causando esto? ¿Toda la región de la memoria para la aplicación se reasignó a sí misma, debido a la reasignación causada por el apéndice SecByteBlock?

Además, This is SecByteBlock's Documentation

edición: Después de comprobar el vaciado de memoria usando vim, tengo esto: http://imgur.com/owkaw

Edit2: código actualizado por lo que es más fácilmente compilables y compilación instrucciones

final edit3: Parece que memcpy es el culpable. Consulte la implementación de Rasmus mymemcpy en su respuesta a continuación.

+0

No creo que sea la razón de lo que estás viendo, pero ¿sabes que llamar a 'memset' en un poco de memoria no impide que todavía haya una copia en algún archivo de intercambio en alguna parte? Y, en general, el resultado del 'memset' no necesita penetrar 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. –

+0

@SteveJessop hmm, esa es probablemente la razón por la que windows tiene SecureZeroMemory o algo así. Me pregunto qué sería el equivalente de Linux/Posix ... si lo hay. – kamziro

+0

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

Respuesta

9

Aquí hay otro programa que reproduce el problema de manera más directa:

#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 reemplaza memcpy con mymemcpy o utilizar tamaños más pequeños el problema desaparece, por lo que mi mejor conjetura es que la orden interna memcpy hace algo que las hojas parte de los datos copiados en la memoria.

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

+0

Maldiciones, eso es desafortunado . Supongo que eso excluye la comercialización de mi aplicación con "¡Seguro contra RAM forense!" – kamziro

+0

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

+0

@kamziro. Pero, curiosamente, el uso de -fno-builtin-memcpy no funciona. –

2

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

Este otro SO pregunta hace un trabajo decente de explicarla: Is a string literal in c++ created in static memory?

Puede tratar de confirmar si los partidos grep pueden ser explicadas por los literales de cadena por ver cuántos partidos que se obtiene. También puede imprimir las ubicaciones de memoria de los búferes SecByteBlock y tratar de ver si se corresponden con las ubicaciones en el volcado del núcleo que coinciden con su marcador.

+0

La coincidencia de la memoria de volcado núcleo se ve más o menos así: "@^@^@^@^@^@^@^@ 0LOLWUT231LOLWUTOLWUT229LOLWUT23216LOLWUT217LOLWUT218LOLWUT219LOLWUT220LOLWUT221LOLWUT222LOLWUT223LOLWUT224LOLWUT225LOLWUT226LOL^@^@^@^@^@^@^@^@ (desde vim), por lo que son definitivamente del secbyteblock. Establecer el bucle a cero elementos no da coincidencias. La configuración a cero también se realiza mediante la entrada del usuario, por lo que definitivamente no está "optimizado para el compilador" – kamziro

+0

Además, esta es la captura de pantalla de vim del núcleo de la coincidencia de volcado: http://imgur.com/owkaw – kamziro

1

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 búferes de memoria pequeños. Puede verificar esto ejecutando un depurador y ver si LOLWUT aparece cuando se visualiza la memoria de la pila.

[La implementación de reallocate en Crypto ++ utiliza memcpy_s al cambiar el tamaño asignaciones de memoria, por lo que sería capaz de encontrar algún número de LOLWUT cadenas en la memoria. Además, el hecho de que muchos diferentes LOLWUT cadenas se superponen en ese volcado sugieren que se trata de un buffer temporal que está siendo reutilizado.]

La versión personalizada de memcpy que es sólo un simple bucle no requiere almacenamiento temporal más allá de contadores, de manera que lo haría sin duda será más seguro que cómo se implementa memcpy_s.

0

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

+0

El compilador "inteligente" suficiente para optimizar 'memset' en la memoria que está a punto de' libre', también podría eliminar el último cifrado. O la próxima versión del compilador podría. –

+0

Te sugiero que solo encriptes/descifres la sección específica que estás leyendo/escribiendo actualmente. –

20

A pesar de aparecer en el núcleo, la contraseña ya no está en la memoria después de borrar los búferes. El problema es que memcpy ing cadena suficientemente larga gotea la contraseña en los registros SSE, y esos son los que aparecen en el núcleo.

Cuando el argumento size a memcpy es mayor que un cierto de umbrales 80 bytes on the mac instrucciones SSE -entonces se utilizan para hacer la copia 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 la Libc on the 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 del bucle, %rsi es el s registro de direcciones ource, y %rdi es el detino registro de direcciones d. Cada corrida alrededor del bucle, 64 bytes se copian desde el búfer fuente a los 4 registros SSE de 16 bytes xmm{0,1,2,3}; entonces los valores en esos registros se copian en el buffer de destino .

Hay mucho más cosas en ese archivo de origen para asegurarse de que las copias se producen sólo en direcciones alineadas, para rellenar la parte de la copia que es de sobra después de hacer trozos de 64 bytes, y para manejar el caso en el que la fuente y superposición de destino.

Sin embargo- ¡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á presente en los registros xmm{0,1,2,3}.

Aquí es 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, esta impresiones:

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

Ahora, examinar el volcado de memoria, la contraseña sólo se produce una sola vez, y como esa cadena exacta 0\0LOLWUT130\0...180\0\0\0. El volcado del núcleo tiene que contener una copia de todos los registros, por lo que esa cadena está allí, son los valores de los registros xmm{0,1,2,4}.

Así la contraseña no es en realidad en la memoria RAM más después de llamar SecureWipeBuffer, sólo parece ser, ya que es en realidad en algunos registros que sólo aparecen en el volcado de núcleo. Si le preocupa que memcpy tenga una vulnerabilidad que pueda ser explotada 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 un memcpy modificado que no use los registros SSE2, o los borre cuando haya terminado. Y si realmente está paranoico acerca de esto, siga probando sus núcleos para asegurarse de que el compilador no está optimizando su código de borrado de contraseña .

Cuestiones relacionadas