Logotipo de Zephyrnet

Seguridad seria: el "crackeo de la contraseña maestra" de KeePass y lo que podemos aprender de él

Fecha:

Durante las últimas dos semanas, hemos visto una serie de artículos que hablan de lo que se ha descrito como un "crackeo de contraseña maestra" en el popular administrador de contraseñas de código abierto KeePass.

El error se consideró lo suficientemente importante como para obtener un identificador oficial del gobierno de EE. UU. (se conoce como CVE-2023-32784, si quiere cazarlo), y dado que la contraseña maestra de su administrador de contraseñas es prácticamente la clave de todo su castillo digital, puede entender por qué la historia provocó tanta emoción.

La buena noticia es que un atacante que quisiera explotar este error seguramente ya tendría que haber infectado su computadora con malware y, por lo tanto, podría espiar sus pulsaciones de teclas y programas en ejecución de todos modos.

En otras palabras, el error puede considerarse un riesgo fácil de manejar hasta que el creador de KeePass presente una actualización, que debería aparecer pronto (a principios de junio de 2023, aparentemente).

Como el revelador del error se encarga de señalar:

Si usa el cifrado de disco completo con una contraseña segura y su sistema está [libre de malware], debería estar bien. Nadie puede robar sus contraseñas de forma remota a través de Internet solo con este hallazgo.

Los riesgos explicados

Muy resumido, el error se reduce a la dificultad de garantizar que todos los rastros de datos confidenciales se eliminen de la memoria una vez que haya terminado con ellos.

Ignoraremos aquí los problemas de cómo evitar tener datos secretos en la memoria, aunque sea brevemente.

En este artículo, solo queremos recordar a los programadores de todo el mundo que el código fue aprobado por un revisor consciente de la seguridad con un comentario como "parece que se limpia correctamente después de sí mismo"...

…de hecho, podría no limpiarse completamente y la posible fuga de datos podría no ser obvia a partir de un estudio directo del código en sí.

En pocas palabras, la vulnerabilidad CVE-2023-32784 significa que una contraseña maestra de KeePass podría recuperarse de los datos del sistema incluso después de que el programa KeyPass haya salido, porque la información suficiente sobre su contraseña (aunque no la contraseña sin procesar en sí, en la que nos centraremos encendido en un momento) puede quedarse atrás en el intercambio del sistema o en los archivos de suspensión, donde la memoria del sistema asignada puede terminar guardada para más adelante.

En una computadora con Windows donde BitLocker no se usa para encriptar el disco duro cuando el sistema está apagado, esto le daría a un ladrón que robó su computadora portátil la oportunidad de arrancar desde una unidad USB o CD, y recuperar su contraseña maestra incluso aunque el propio programa KeyPass se encarga de nunca guardarlo permanentemente en el disco.

Una fuga de contraseña a largo plazo en la memoria también significa que la contraseña podría, en teoría, recuperarse de un volcado de memoria del programa KeyPass, incluso si ese volcado se tomó mucho después de haber ingresado la contraseña y mucho después de que KeePass en sí mismo ya no tenía necesidad de mantenerlo alrededor.

Claramente, debe suponer que el malware que ya está en su sistema podría recuperar casi cualquier contraseña ingresada a través de una variedad de técnicas de espionaje en tiempo real, siempre que estuvieran activas en el momento en que las escribió. Pero es razonable esperar que su tiempo expuesto al peligro se limite al breve período de escribir, no se extienda a muchos minutos, horas o días después, o quizás más, incluso después de apagar la computadora.

¿Qué se queda atrás?

Por lo tanto, pensamos en echar un vistazo de alto nivel a cómo los datos secretos pueden quedar en la memoria de maneras que no son directamente obvias en el código.

No se preocupe si no es programador: lo simplificaremos y lo explicaremos a medida que avanzamos.

Comenzaremos observando el uso y la limpieza de la memoria en un programa C simple que simula ingresar y almacenar temporalmente una contraseña haciendo lo siguiente:

  • Asignación de una porción de memoria dedicada especialmente para almacenar la contraseña.
  • Insertar una cadena de texto conocida para que podamos encontrarlo fácilmente en la memoria si es necesario.
  • Adición de 16 caracteres ASCII de 8 bits pseudoaleatorios de la gama AP.
  • Imprimiendolo el búfer de contraseña simulado.
  • Liberando la memoria con la esperanza de borrar el búfer de contraseña.
  • Salir el programa.

Muy simplificado, el código C podría parecerse a esto, sin verificación de errores, utilizando números pseudoaleatorios de baja calidad de la función de tiempo de ejecución C rand(), e ignorando las comprobaciones de desbordamiento del búfer (¡nunca haga nada de esto en código real!):

 // Ask for memory char* buff = malloc(128); // Copy in fixed string we can recognise in RAM strcpy(buff,"unlikelytext"); // Append 16 pseudo-random ASCII characters for (int i = 1; i <= 16; i++) { // Choose a letter from A (65+0) to P (65+15) char ch = 65 + (rand() & 15); // Modify the buff string directly in memory strncat(buff,&ch,1); } // Print it out, so we're done with buff printf("Full string was: %sn",buff); // Return the unwanted buffer and hope that expunges it free(buff);

De hecho, el código que finalmente usamos en nuestras pruebas incluye algunos bits y piezas adicionales que se muestran a continuación, para que podamos volcar el contenido completo de nuestro búfer de contraseña temporal a medida que lo usamos, para buscar contenido sobrante o no deseado.

Tenga en cuenta que deliberadamente volcamos el búfer después de llamar free(), que técnicamente es un error de uso después de liberar, pero lo estamos haciendo aquí como una forma disimulada de ver si queda algo crítico después de devolver nuestro búfer, lo que podría conducir a un peligroso agujero de fuga de datos en la vida real.

También hemos insertado dos Waiting for [Enter] indicaciones en el código para darnos la oportunidad de crear volcados de memoria en puntos clave del programa, brindándonos datos sin procesar para buscar más tarde, a fin de ver qué quedó mientras se ejecutaba el programa.

Para hacer volcados de memoria, usaremos Microsoft Herramienta Sysinternals procdump con el -ma opción (volcar toda la memoria), lo que evita la necesidad de escribir nuestro propio código para usar Windows DbgHelp sistema y es bastante complejo MiniDumpXxxx() funciones.

Para compilar el código C, usamos nuestra propia compilación pequeña y simple del código abierto y gratuito de Fabrice Bellard. Compilador minúsculo de C, disponible para Windows de 64 bits en fuente y forma binaria directamente desde nuestra página de GitHub.

El texto que se puede copiar y pegar de todo el código fuente que se muestra en el artículo aparece en la parte inferior de la página.

Esto es lo que sucedió cuando compilamos y ejecutamos el programa de prueba:

C:UsersduckKEYPASS> petcc64 -stdinc -stdlib unl1.c Compilador Tiny C - Copyright (C) 2001-2023 Fabrice Bellard Desmontado por Paul Ducklin para su uso como herramienta de aprendizaje Versión petcc64-0.9.27 [0006] - Genera 64 bits Solo PE -> unl1.c -> c:/users/duck/tcc/petccinc/stdio.h [. . . .] -> c:/usuarios/duck/tcc/petcclib/libpetcc1_64.a -> C:/Windows/system32/msvcrt.dll -> C:/Windows/system32/kernel32.dll -------- -------------- sección de tamaño de archivo virt 1000 200 438 .text 2000 800 2ac .data 3000 c00 24 .pdata -------- ----------------------- <- unl1.exe (3584 bytes) C:UsersduckKEYPASS> unl1.exe Dumping 'new' buffer at start 00F51390: 90 57 F5 00 00 00 00 00 50 01 F5 00 00 00 00 00 .W......P....... 00F513A0: 73 74 65 6D 33 32 5C 63 6D 64 2E 65 78 65 00 44 tallo32cmd. ej.D 00F513B0: 72 69 76 65 72 44 61 74 61 3D 43 3A 5C 57 69 6E riverData=C:Win 00F513C0: 64 6F 77 73 5C 53 79 73 74 65 6D 33 32 5C 44 72 dowsSystem32Dr 00F513D0: 69 76 65 72 73 5C 44 72 69 76 65 72 44 61 74 61 iversDriverData 00F513E0: 00 45 46 43 5F 34 33 37 32 3D 31 00 46 50 53 5F .EFC_4372=1.FPS_ 00F513 0F42: 52 4 57F 53 45 52 5 41F 50 50 5 50F 52 4 46F 00 BROWSER_APP_PROF 51400F49: 4 45C 5 53F 54 52 49 4 47E 3 49D 6 74E 65 72 00 ILE_STRING=Inter 51410F6: 65E 74 20 45 78 70 6 7C 56A 4 F3 4C AC 00B 00 00 net ExplzV.< .K.. La cadena completa era: textoimprobableJHKNEJJCPOMDJHAN 51390F75: 6 6E 69C 6 65B 6 79C 74 65 78 74 4 48A 4 4B 00E textoimprobableJHKN 513F0A45: 4 4A 43A 50 4 4F 44D 4 48A 41 4 00E 65 00 44 00 EJJCPOMDJHAN.eD 513F0B72 : 69 76 65 72 44 61 74 61 3 43D 3 5A 57C 69 6 00E riverData=C:Win 513F0C64: 6 77F 73 5 53C 79 73 74 65 6 33D 32 5 44C 72 32 dowsSystem00D 513F0D69: 76 65 72 73 5 44C 72 69 76 65 72 44 61 74 61 00 iversDriverData 513F0E00: 45 46 43 5 34F 33 37 32 3 31D 00 46 50 53 5 4372F .EFC_1=00.FPS_ 513F0F42: 52 4 57F 53 45 52 5 41F 50 50 5 50F 52 4 46F 00 BROWSER_APP_PROF 51400F49: 4 45C 5 53F 54 52 49 4 47E 3 49D 6 74E 65 72 00 ILE_STRING=Inter 51410F6: 65E 74 20 45 78 70 6 7C 56A 4 F 3 4C AC 00B 00 00 net ExplzV.<.K.. Esperando [ENTER] para liberar el búfer... Volcar el búfer después de liberar() 51390F0: A67 5 F00 00 00 00 00 50 01 5 F00 00 00 00 00 00 .g......P...... 513F0A45: 4 4A 43A 50 4 4F 44D 4 48A 41 4 00E 65 00 44 00 EJJCPOMDJHAN.eD 513F0B72: 69 76 65 72 44 61 74 61 3 43D 3 5A 57C 69 6 00 513E riverData=C:Gana 0F64C6: 77 73F 5 53 79C 73 74 65 6 33 32D 5 44 72C 32 00 dowsSystem513Dr 0F69D76: 65 72 73 5 44 72C 69 76 65 72 44 61 74 61 00 513 0 00 45 46 43F 5 34 33 37 32D 3 31 00 46 50 53F .EFC_5=4372.FPS_ 1F00F513: 0 42 52F 4 57 53 45 52F 5 41 50 50F 5 50 52F 4 BROWSER_APP_PROF 46F00: 51400 49C 4 45F 5 53 54 52 49 E 4 47D 3 49E 6 74 65 ILE_STRING=Entre 72F00: 51410E 6 65 74 20 45 78 70C 6D 4 00 00D AC 4B 4 00 red ExplM..MK. Esperando [ENTER] para salir de main()... C:UsersduckKEYPASS>

En esta ejecución, no nos molestamos en obtener ningún volcado de memoria del proceso, porque pudimos ver de inmediato en el resultado que este código filtra datos.

Inmediatamente después de llamar a la función de biblioteca de tiempo de ejecución de Windows C malloc(), podemos ver que el búfer que recuperamos incluye lo que parecen datos de variables de entorno sobrantes del código de inicio del programa, con los primeros 16 bytes aparentemente alterados para que parezcan algún tipo de encabezado de asignación de memoria sobrante.

(Observe cómo esos 16 bytes parecen dos direcciones de memoria de 8 bytes, 0xF55790 y 0xF50150, que están justo después y justo antes de nuestro propio búfer de memoria, respectivamente).

Cuando se supone que la contraseña está en la memoria, podemos ver claramente la cadena completa en el búfer, como era de esperar.

Pero despues de llamar free(), observe cómo los primeros 16 bytes de nuestro búfer se han reescrito con lo que parecen direcciones de memoria cercanas una vez más, presumiblemente para que el asignador de memoria pueda realizar un seguimiento de los bloques en la memoria que puede reutilizar...

… pero el resto de nuestro texto de contraseña “borrado” (los últimos 12 caracteres aleatorios EJJCPOMDJHAN) se ha quedado atrás.

No solo necesitamos administrar nuestras propias asignaciones y desasignaciones de memoria en C, también debemos asegurarnos de elegir las funciones correctas del sistema para los búferes de datos si queremos controlarlos con precisión.

Por ejemplo, al cambiar a este código, tenemos un poco más de control sobre lo que hay en la memoria:

Al cambiar de malloc() y free() para usar las funciones de asignación de Windows de nivel inferior VirtualAlloc() y VirtualFree() directamente, obtenemos un mejor control.

Sin embargo, pagamos un precio en velocidad, porque cada llamada a VirtualAlloc() hace más trabajo que una llamada a malloc(), que funciona dividiendo y subdividiendo continuamente un bloque de memoria de bajo nivel preasignada.

Usar VirtualAlloc() repetidamente para bloques pequeños también usa más memoria en general, porque cada bloque repartido por VirtualAlloc() normalmente consume un múltiplo de 4 KB de memoria (o 2 MB, si está utilizando los llamados grandes páginas de memoria), por lo que nuestro búfer de 128 bytes anterior se redondea a 4096 bytes, desperdiciando los 3968 bytes al final del bloque de memoria de 4 KB.

Pero, como puede ver, la memoria que recuperamos se borra automáticamente (se pone a cero), por lo que no podemos ver lo que había allí antes, y esta vez el programa falla cuando intentamos hacer nuestro uso después de la liberación. truco, porque Windows detecta que estamos tratando de echar un vistazo a la memoria que ya no poseemos:

C:UsersduckKEYPASS> unl2 Volcado de búfer 'nuevo' al inicio .. .............. 0000000000EA0000: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0000000000EA0010: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0000000000EA0020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0000000000EA0030: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0000000000EA0040: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ 0000000000EA0050: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0000000000 ................ La cadena completa era: textoimprobableIBIPJPPHEOPOIDLL 0060EA00: 00 00E 00C 00 00B 00 00C 00 00 00 00 00 00 00 00 0000000000 textoimprobableIBIP 0070EA00: 00A 00 00 00 00 00 00F 00 00F 00 00 00C 00C 00 00 0000000000 0080 JPPHEOFOUCHLL ... 00EA00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0000000000 0000 ................ 75 : 6 6 69 6 65 6 79 74 65 78 74 49 42 49 50 0000000000 ................ 0010EA4: 50 50 48 45 4 50 4 49 44 4 4 00 00 00 00 0000000000 ................ 0020EA00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 0000000000 ............... . 0030 00 00 00 ................ 00EA00: 00 00 00 00 00 00 00 00 00 00 00 0000000000 0040 00 00 00 ............. ... Esperando [ENTER] para liberar el búfer... Dumping buffer after free() 00EA00: [El programa finalizó aquí porque Windows detectó nuestro uso después de liberar]

Debido a que la memoria que liberamos necesitará reasignarse con VirtualAlloc() antes de que pueda volver a usarse, podemos suponer que se pondrá a cero antes de que se recicle.

Sin embargo, si quisiéramos asegurarnos de que estaba en blanco, podríamos llamar a la función especial de Windows RtlSecureZeroMemory() justo antes de liberarlo, para garantizar que Windows escriba ceros en nuestro búfer primero.

La función relacionada RtlZeroMemory(), si se lo preguntaba, hace algo similar, pero sin la garantía de que realmente funcione, porque los compiladores pueden eliminarlo como teóricamente redundante si notan que el búfer no se vuelve a usar después.

Como puede ver, debemos tener mucho cuidado para usar las funciones correctas de Windows si queremos minimizar el tiempo que los secretos almacenados en la memoria pueden permanecer para más adelante.

En este artículo, no veremos cómo evitar que los secretos se guarden accidentalmente en su archivo de intercambio bloqueándolos en la memoria RAM física. (Pista: VirtualLock() en realidad no es suficiente por sí solo). Si desea obtener más información sobre la seguridad de la memoria de Windows de bajo nivel, háganoslo saber en los comentarios y lo veremos en un artículo futuro.

Uso de la gestión automática de memoria

Una buena manera de evitar tener que asignar, administrar y desasignar memoria por nosotros mismos es usar un lenguaje de programación que se encargue de malloc() y free()o VirtualAlloc() y VirtualFree(), de forma automática.

Lenguaje de secuencias de comandos como Perl, Python, Lua, JavaScript y otros se deshacen de los errores de seguridad de memoria más comunes que plagan el código C y C++, mediante el seguimiento del uso de la memoria en segundo plano.

Como mencionamos anteriormente, nuestro código C de muestra mal escrito anterior funciona bien ahora, pero solo porque sigue siendo un programa súper simple, con estructuras de datos de tamaño fijo, donde podemos verificar mediante inspección que no sobrescribiremos nuestro 128- búfer de bytes, y que solo hay una ruta de ejecución que comienza con malloc() y termina con el correspondiente free().

Pero si lo actualizamos para permitir la generación de contraseñas de longitud variable, o agregamos características adicionales al proceso de generación, entonces nosotros (o quien mantenga el código a continuación) fácilmente podríamos terminar con desbordamientos de búfer, errores de uso después de liberar o memoria que nunca se libera y, por lo tanto, deja datos secretos dando vueltas mucho después de que ya no se necesitan.

En un lenguaje como Lua, podemos dejar que el entorno de tiempo de ejecución de Lua, que hace lo que se conoce en la jerga como recolección automática de basura, se ocupa de adquirir memoria del sistema y devolverla cuando detecta que hemos dejado de usarla.

El programa en C que enumeramos anteriormente se vuelve mucho más simple cuando nos ocupamos de la asignación y desasignación de memoria:

Asignamos memoria para contener la cadena. s simplemente asignando la cadena 'unlikelytext' a la misma.

Más tarde podemos insinuar explícitamente a Lua que ya no estamos interesados ​​en s asignándole el valor nil (todos nils son esencialmente el mismo objeto Lua), o dejar de usar s y espera a que Lua detecte que ya no es necesario.

De cualquier manera, la memoria utilizada por s eventualmente se recuperará automáticamente.

Y para evitar desbordamientos de búfer o mala gestión del tamaño al agregar cadenas de texto (el operador Lua ..pronunciado concatenar, esencialmente agrega dos cadenas juntas, como + en Python), cada vez que extendemos o acortamos una cadena, Lua mágicamente asigna espacio para una nueva cadena, en lugar de modificar o reemplazar la original en su ubicación de memoria existente.

Este enfoque es más lento y conduce a picos de uso de memoria que son más altos de lo que obtendría en C debido a las cadenas intermedias asignadas durante la manipulación del texto, pero es mucho más seguro con respecto a los desbordamientos de búfer.

Pero este tipo de gestión automática de cadenas (conocida en la jerga como inmutabilidad, porque las cadenas nunca se ponen mutado, o modificados en su lugar, una vez que se han creado), trae consigo nuevos dolores de cabeza de ciberseguridad.

Ejecutamos el programa Lua anterior en Windows, hasta la segunda pausa, justo antes de que se cerrara el programa:

C:UsersduckKEYPASS> lua s1.lua La cadena completa es: textoimprobableHLKONBOJILAGLNLN Esperando [ENTER] antes de liberar la cadena... Esperando [ENTER] antes de salir...

Esta vez, hicimos un volcado de memoria de proceso, así:

C:UsersduckKEYPASS> procdump -ma lua lua-s1.dmp ProcDump v11.0 - Utilidad de volcado de procesos de Sysinternals Copyright (C) 2009-2022 Mark Russinovich y Andrew Richards Sysinternals - www.sysinternals.com [00:00:00] Volcado 1 iniciado: C:UsersduckKEYPASSlua-s1.dmp [00:00:00] Escritura de volcado 1: el tamaño estimado del archivo de volcado es de 10 MB. [00:00:00] Volcado 1 completo: 10 MB escritos en 0.1 segundos [00:00:01] Contador de volcado alcanzado.

Luego ejecutamos este script simple, que lee el archivo de volcado nuevamente, encuentra en todas partes en la memoria que la cadena conocida unlikelytext apareció y lo imprime, junto con su ubicación en el archivo de volcado y los caracteres ASCII que siguen inmediatamente:

Incluso si ha usado lenguajes de script anteriormente o ha trabajado en cualquier ecosistema de programación que presente los llamados cadenas administradas, donde el sistema realiza un seguimiento de las asignaciones y desasignaciones de memoria por usted, y las maneja como mejor le parezca...

…es posible que se sorprenda al ver el resultado que produce este escaneo de memoria:

C:UsersduckKEYPASS> lua findit.lua lua-s1.dmp 006D8AFC: texto poco probable ALJBNGOAPLLBDEB 006D8B3C: texto poco probable ALJBNGOA 006D8B7C: texto poco probable ALJBNGO 006D8BFC: texto poco probable ALJBNGOAPLLBDEBJ 006D8CBC: texto poco probable ALJBN 006D8D7C: textopoco probableALJBNGOAP 006D903C:textopoco probableALJBNGOAPL 006D90BC:textopoco probableALJBNGOAPLL 006D90FC:textopoco probableALJBNG 006D913C:textopoco probableALJBNGOAPLLB 006D91BC:textopoco probableALJB 006D91FC:textopoco probableALJBNGOAPLLBD 006D923C : textoimprobableALJBNGOAPLLBDE 006DB70C:textoimprobableALJ 006DBB8C:textoimprobableAL 006DBD0C:textoimprobableA

He aquí, en el momento en que tomamos nuestro volcado de memoria, a pesar de que habíamos terminado con la cadena s (y le dije a Lua que ya no lo necesitábamos diciendo s = nil), todas las cadenas que el código había creado en el camino todavía estaban presentes en la RAM, aún no recuperadas o eliminadas.

De hecho, si ordenamos la salida anterior por las cadenas en sí, en lugar de seguir el orden en que aparecieron en la RAM, podrá imaginarse lo que sucedió durante el bucle en el que concatenamos un carácter a la vez con nuestra cadena de contraseña:

C:UsersduckKEYPASS> lua findit.lua lua-s1.dmp | sort /+10 006DBD0C: textoimprobableA 006DBB8C:textoimprobableAL 006DB70C:textoimprobableALJ 006D91BC:textoimprobableALJB 006D8CBC:textoimprobableALJBN 006D90FC:textoimprobableALJBNG 006D8B7C:textoimprobableALJBNGO 006D8B3C:textoimprobableALJBNGOA 006 D8D7C: texto poco probable ALJBNGOAP 006D903C: texto poco probable ALJBNGOAPL 006D90BC: texto poco probable ALJBNGOAPLL 006D913C: texto poco probable ALJBNGOAPLLB 006D91FC: texto poco probable ALJBNGOAPLLBD 006D923C: texto poco probable ALJBNGOAPLLBDE 006D8AFC: texto poco probable ALJBNGOAPLLBD EB 006D8BFC : textoimprobableALJBNGOAPLLBDEBJ

Todas esas cadenas intermedias temporales todavía están allí, por lo que incluso si hubiéramos borrado con éxito el valor final de s, todavía estaríamos filtrando todo excepto su último carácter.

De hecho, en este caso, incluso cuando forzamos deliberadamente a nuestro programa a deshacerse de todos los datos innecesarios llamando a la función especial de Lua collectgarbage() (la mayoría de los lenguajes de secuencias de comandos tienen algo similar), la mayoría de los datos en esas cadenas temporales molestas se quedaron en la RAM de todos modos, porque habíamos compilado Lua para hacer su gestión de memoria automática usando buenos viejos malloc() y free().

En otras palabras, incluso después de que el propio Lua recuperara sus bloques de memoria temporal para volver a utilizarlos, no pudimos controlar cómo o cuándo se reutilizarían esos bloques de memoria y, por lo tanto, cuánto tiempo permanecerían dentro del proceso con su lado izquierdo. sobre los datos que esperan ser detectados, volcados o filtrados de otra manera.

Ingrese .NET

Pero, ¿qué pasa con KeePass, que es donde comenzó este artículo?

KeePass está escrito en C# y utiliza el tiempo de ejecución .NET, por lo que evita los problemas de mala gestión de la memoria que traen consigo los programas C…

…pero C# administra sus propias cadenas de texto, como lo hace Lua, lo que plantea la pregunta:

Incluso si el programador evitara almacenar la contraseña maestra completa en un lugar después de haberla terminado, ¿podrían los atacantes con acceso a un volcado de memoria encontrar suficientes datos temporales sobrantes para adivinar o recuperar la contraseña maestra de todos modos, incluso si esos ¿Los atacantes obtuvieron acceso a su computadora minutos, horas o días después de que usted ingresó la contraseña?

En pocas palabras, ¿hay restos detectables y fantasmales de su contraseña maestra que sobreviven en la RAM, incluso después de que esperaría que se eliminaran?

Molesto, como usuario de Github Vdohney descubrió, la respuesta (al menos para las versiones de KeePass anteriores a la 2.54) es "Sí".

Para ser claros, no creemos que su contraseña maestra real pueda recuperarse como una sola cadena de texto de un volcado de memoria de KeePass, porque el autor creó una función especial para la entrada de contraseña maestra que hace todo lo posible para evitar almacenar el archivo completo. contraseña donde podría ser detectada y olfateada fácilmente.

Nos conformamos con esto configurando nuestra contraseña maestra en SIXTEENPASSCHARS, escribiéndolo y luego tomando volcados de memoria inmediatamente, poco y mucho después.

Buscamos en los vertederos con un simple script Lua que buscó en todas partes el texto de la contraseña, tanto en formato ASCII de 8 bits como en formato UTF-16 de 16 bits (Windows widechar), así:

Los resultados fueron alentadores:

C:UsersduckKEYPASS> lua searchknown.lua kp2-post.dmp Leyendo en el archivo de volcado... HECHO. Buscando SIXTEENPASSCHARS como ASCII de 8 bits... no encontrado. Buscando SIXTEENPASSCHARS como UTF-16... no encontrado.

Pero Vdohney, el descubridor de CVE-2023-32784, notó que a medida que ingresa su contraseña maestra, KeePass le brinda información visual al construir y mostrar una cadena de marcador de posición que consta de caracteres "blob" Unicode, hasta e incluyendo la longitud de su contraseña:

En cadenas de texto widechar en Windows (que consisten en dos bytes por carácter, no solo un byte cada uno como en ASCII), el carácter "blob" se codifica en la RAM como el byte hexadecimal 0xCF seguido por 0x25 (que resulta ser un signo de porcentaje en ASCII).

Por lo tanto, incluso si KeePass tiene mucho cuidado con los caracteres sin procesar que ingresa cuando ingresa la contraseña, es posible que termine con cadenas sobrantes de caracteres "blob", fácilmente detectables en la memoria como ejecuciones repetidas como CF25CF25 or CF25CF25CF25...

… y, si es así, la secuencia más larga de caracteres de blob que encontró probablemente revelaría la longitud de su contraseña, lo que sería una forma modesta de fuga de información de contraseña, al menos.

Usamos el siguiente script de Lua para buscar signos de cadenas de marcadores de posición de contraseña sobrantes:

El resultado fue sorprendente (hemos eliminado líneas sucesivas con el mismo número de blobs, o con menos blobs que la línea anterior, para ahorrar espacio):

C:UsersduckKEYPASS> lua findblobs.lua kp2-post.dmp 000EFF3C: * [. . .] 00BE621B: ** 00BE64C7: *** [. . .] 00BE6E8F: **** [. . .] 00BE795F: ***** [. . .] 00BE84F7: ****** [. . .] 00BE8F37: ******* [ continúa de manera similar para 8 blobs, 9 blobs, etc. ] [ hasta dos líneas finales de exactamente 16 blobs cada una ] 00C0503B: ************* *** 00C05077: **************** 00C09337: * 00C09738: * [todas las coincidencias restantes tienen una longitud de blob] 0123B058: *

En direcciones de memoria cercanas pero cada vez mayores, encontramos una lista sistemática de 3 blobs, luego 4 blobs, y así sucesivamente hasta 16 blobs (la longitud de nuestra contraseña), seguida de muchas instancias dispersas aleatoriamente de cadenas de un solo blob .

Por lo tanto, esas cadenas de "manchas" de marcador de posición parecen estar filtrándose en la memoria y quedándose atrás para filtrar la longitud de la contraseña, mucho después de que el software KeePass haya terminado con su contraseña maestra.

El siguiente paso

Decidimos profundizar más, al igual que hizo Vdohney.

Cambiamos nuestro código de coincidencia de patrones para detectar cadenas de caracteres blob seguidos de cualquier carácter ASCII único en formato de 16 bits (los caracteres ASCII se representan en UTF-16 como su código ASCII habitual de 8 bits, seguido de un byte cero).

Esta vez, para ahorrar espacio, hemos suprimido la salida de cualquier coincidencia que coincida exactamente con la anterior:

Sorpresa sorpresa:

C:UsersduckKEYPASS> lua searchkp.lua kp2-post.dmp
00BE581B: *I
00BE621B: **X
00BE6BD3: ***T
00BE769B: ****E
00BE822B: *****E
00BE8C6B: ******N
00BE974B: *******P
00BEA25B: ********A
00BEAD33: *********S
00BEB81B: **********S
00BEC383: ***********C
00BECEEB: ************H
00BEDA5B: *************A
00BEE623: **************R
00BEF1A3: ***************S
03E97CF2: *N
0AA6F0AF: *W
0D8AF7C8: *X
0F27BAF8: *S

¡Mire lo que obtenemos de la región de memoria de cadena administrada de .NET!

Un conjunto compacto de "cadenas de blob" temporales que revelan los caracteres sucesivos de nuestra contraseña, comenzando con el segundo carácter.

Esas cadenas con fugas son seguidas por coincidencias de un solo carácter ampliamente distribuidas que suponemos que surgieron por casualidad. (Un archivo de volcado de KeePass tiene un tamaño de aproximadamente 250 MB, por lo que hay mucho espacio para que aparezcan los caracteres "blob" como por suerte).

Incluso si tomamos en cuenta esas cuatro coincidencias adicionales, en lugar de descartarlas como posibles discrepancias, podemos suponer que la contraseña maestra es una de las siguientes:

?IXTEENPASSCHARS ?NXTEENPASSCHARS ?WXTEENPASSCHARS ?SXTEENPASSCHARS

Obviamente, esta técnica simple no encuentra el primer carácter en la contraseña, porque la primera "cadena de blob" solo se construye después de que se haya ingresado ese primer carácter.

Tenga en cuenta que esta lista es agradable y corta porque filtramos las coincidencias que no terminaron en caracteres ASCII.

Si estaba buscando caracteres en un rango diferente, como caracteres chinos o coreanos, podría terminar con más coincidencias accidentales, porque hay muchos más caracteres posibles para hacer coincidir...

…pero sospechamos que se acercará bastante a su contraseña maestra de todos modos, y las "cadenas de blob" que se relacionan con la contraseña parecen estar agrupadas en la RAM, presumiblemente porque fueron asignadas aproximadamente al mismo tiempo por la misma parte de el tiempo de ejecución de .NET.

Y ahí, en pocas palabras ciertamente largas y discursivas, está la fascinante historia de CVE-2023-32784.

¿Qué hacer?

  • Si eres usuario de KeePass, no entres en pánico. Aunque esto es un error, y técnicamente es una vulnerabilidad explotable, los atacantes remotos que quisieran descifrar su contraseña usando este error primero tendrían que implantar malware en su computadora. Eso les daría muchas otras formas de robar sus contraseñas directamente, incluso si este error no existiera, por ejemplo, registrando sus pulsaciones de teclas a medida que escribe. En este punto, simplemente puede estar atento a la próxima actualización y tomarla cuando esté lista.
  • Si no está utilizando el cifrado de disco completo, considere habilitarlo. Para extraer las contraseñas sobrantes de su archivo de intercambio o archivo de hibernación (archivos de disco del sistema operativo que se usan para guardar el contenido de la memoria temporalmente durante una carga pesada o cuando su computadora está "en reposo"), los atacantes necesitarían acceso directo a su disco duro. Si tiene activado BitLocker o su equivalente para otros sistemas operativos, no podrán acceder a su archivo de intercambio, a su archivo de hibernación ni a ningún otro dato personal, como documentos, hojas de cálculo, correos electrónicos guardados, etc.
  • Si es programador, manténgase informado sobre los problemas de administración de memoria. No asuma que solo porque cada free() coincide con su correspondiente malloc() que sus datos están seguros y bien administrados. A veces, es posible que deba tomar precauciones adicionales para evitar dejar datos secretos por ahí, y esas precauciones varían de un sistema operativo a otro.
  • Si es un probador de control de calidad o un revisor de código, siempre piense "detrás de escena". Incluso si el código de administración de memoria parece ordenado y bien equilibrado, tenga en cuenta lo que sucede detrás de escena (porque es posible que el programador original no lo supiera) y prepárese para hacer un trabajo de estilo pentesting, como el monitoreo del tiempo de ejecución y la memoria. dumping para verificar que el código seguro realmente se está comportando como se supone que debe hacerlo.

CÓDIGO DEL ARTÍCULO: UNL1.C

#incluir #incluir #incluir void hexdump(unsigned char* buff, int len) { // Imprime el búfer en fragmentos de 16 bytes para (int i = 0; i < len+16; i = i+16) { printf("%016X: ",buff +yo); // Mostrar 16 bytes como valores hexadecimales para (int j = 0; j < 16; j = j+1) { printf("%02X ",buff[i+j]); } // Repita esos 16 bytes como caracteres para (int j = 0; j < 16; j = j+1) { unsigned ch = buff[i+j]; printf("%c",(ch>=32 && ch<=127)?ch:'.'); } printf("n"); } printf("n"); } int main(void) { // Adquiere memoria para almacenar la contraseña y muestra lo que // está en el búfer cuando es oficialmente "nuevo"... char* buff = malloc(128); printf("Volcando el búfer 'nuevo' al inicio"); volcado hexadecimal (mejora, 128); // Usar dirección de búfer pseudoaleatoria como semilla aleatoria srand((unsigned)buff); // Inicie la contraseña con algún texto fijo que se pueda buscar strcpy(buff,"unlikelytext"); // Agrega 16 letras pseudoaleatorias, una a la vez para (int i = 1; i <= 16; i++) { // Elige una letra de A (65+0) a P (65+15) char ch = 65 + (al azar() y 15); // Luego modifique la cadena buff en su lugar strncat(buff,&ch,1); } // La contraseña completa ahora está en la memoria, así que // imprímala como una cadena y muestre el búfer completo... printf("La cadena completa era: %sn",buff); volcado hexadecimal (mejora, 128); // Haga una pausa para volcar la memoria RAM del proceso ahora (pruebe: 'procdump -ma') puts("Esperando [ENTER] para liberar el búfer..."); getchar(); // Formalmente libere() la memoria y muestre el búfer // nuevamente para ver si algo quedó atrás... free(buff); printf("Dumping buffer after free()n"); volcado hexadecimal (mejora, 128); // Haga una pausa para volver a volcar la RAM para inspeccionar las diferencias puts("Esperando que [ENTER] salga de main()..."); getchar(); devolver 0; }

CÓDIGO DEL ARTÍCULO: UNL2.C

#incluir #incluir #incluir #incluir void hexdump(unsigned char* buff, int len) { // Imprime el búfer en fragmentos de 16 bytes para (int i = 0; i < len+16; i = i+16) { printf("%016X: ",buff +yo); // Mostrar 16 bytes como valores hexadecimales para (int j = 0; j < 16; j = j+1) { printf("%02X ",buff[i+j]); } // Repita esos 16 bytes como caracteres para (int j = 0; j < 16; j = j+1) { unsigned ch = buff[i+j]; printf("%c",(ch>=32 && ch<=127)?ch:'.'); } printf("n"); } printf("n"); } int main(void) { // Adquirir memoria para almacenar la contraseña y mostrar // qué hay en el búfer cuando es oficialmente "nuevo"... char* buff = VirtualAlloc(0,128,MEM_COMMIT,PAGE_READWRITE); printf("Volcando el búfer 'nuevo' al inicio"); volcado hexadecimal (mejora, 128); // Usar dirección de búfer pseudoaleatoria como semilla aleatoria srand((unsigned)buff); // Inicie la contraseña con algún texto fijo que se pueda buscar strcpy(buff,"unlikelytext"); // Agrega 16 letras pseudoaleatorias, una a la vez para (int i = 1; i <= 16; i++) { // Elige una letra de A (65+0) a P (65+15) char ch = 65 + (al azar() y 15); // Luego modifique la cadena buff en su lugar strncat(buff,&ch,1); } // La contraseña completa ahora está en la memoria, así que // imprímala como una cadena y muestre el búfer completo... printf("La cadena completa era: %sn",buff); volcado hexadecimal (mejora, 128); // Haga una pausa para volcar la memoria RAM del proceso ahora (pruebe: 'procdump -ma') puts("Esperando [ENTER] para liberar el búfer..."); getchar(); // Formalmente libere() la memoria y muestre el búfer // nuevamente para ver si algo quedó atrás... VirtualFree(buff,0,MEM_RELEASE); printf("Dumping buffer after free()n"); volcado hexadecimal (mejora, 128); // Haga una pausa para volver a volcar la RAM para inspeccionar las diferencias puts("Esperando que [ENTER] salga de main()..."); getchar(); devolver 0; }

CÓDIGO DEL ARTÍCULO: S1.LUA

-- Comience con un texto fijo que se pueda buscar s = 'unlikelytext' -- Agregue 16 caracteres aleatorios de 'A' a 'P' para i = 1,16 do s = s .. string.char(65+math.random( 0,15)) end print('La cadena completa es:',s,'n') -- Pausa para volcar la memoria RAM del proceso print('Esperando [ENTRAR] antes de liberar la cadena...') io.read() - - Limpie la cadena y marque la variable como no utilizada s = nil -- Vuelva a volcar la RAM para buscar diferencias print('Esperando [ENTRAR] antes de salir...') io.read()

CÓDIGO DEL ARTÍCULO: FINDIT.LUA

-- leer en el archivo de volcado local f = io.open(arg[1],'rb'):read('*a') -- buscar el texto del marcador seguido de uno -- o más caracteres ASCII aleatorios local b,e ,m = 0,0,nil while true do -- buscar la siguiente coincidencia y recordar el desplazamiento b,e,m = f:find('(texto poco probable[AZ]+)',e+1) -- salir cuando ya no haya más coincide si no es b, luego rompe el final -- informa la posición y la cadena encontrada print(string.format('%08X: %s',b,m)) end

CÓDIGO DEL ARTÍCULO: SEARCHKNOWN.LUA

io.write('Leyendo en el archivo de volcado...') local f = io.open(arg[1],'rb'):read('*a') io.write('DONE.n') io. write('Buscando SIXTEENPASSCHARS como ASCII de 8 bits...') local p08 = f:find('SIXTEENPASSCHARS') io.write(p08 y 'FOUND' o 'not found','.n') io.write ('Buscando SIXTEENPASSCHARS como UTF-16... ') local p16 = f:find('Sx00Ix00Xx00Tx00Ex00Ex00Nx00Px00'.. 'Ax00Sx00Sx00Cx00Hx00Ax00Rx00Sx00') io.write(p16 y 'FOUND' o 'not encontrado','.n')

CÓDIGO DEL ARTÍCULO: FINDBLOBS.LUA

-- lea en el archivo de volcado especificado en la línea de comandos local f = io.open(arg[1],'rb'):read('*a') -- Busque uno o más blobs de contraseña, seguidos de cualquier blob que no sea -- Tenga en cuenta que los caracteres blob (●) se codifican en caracteres anchos de Windows -- como códigos litte-endian UTF-16, que salen como CF 25 en hexadecimal. local b,e,m = 0,0,nil while true do -- Queremos uno o más blobs, seguidos de cualquier no blob. -- Simplificamos el código buscando un CF25 explícito -- seguido de cualquier cadena que solo tenga CF o 25 -- así que encontraremos CF25CFCF o CF2525CF así como CF25CF25. -- Filtraremos los "falsos positivos" más adelante, si los hay. -- ¡Necesitamos escribir '%%' en lugar de x25 porque el carácter x25 (signo de porcentaje) es un carácter de búsqueda especial en Lua! b,e,m = f:find('(xCF%%[xCF%%]*)',e+1) -- salir cuando no haya más coincidencias si no es b entonces romper el final -- CMD.EXE no puede imprimir blobs, por lo que los convertimos en estrellas. imprimir(cadena.formato('%08X: %s',b,m:gsub('xCF%%','*'))) fin

CÓDIGO DEL ARTÍCULO: SEARCHKP.LUA

-- leer en el archivo de volcado especificado en la línea de comando local f = io.open(arg[1],'rb'):read('*a') local b,e,m,p = 0,0,nil,nil while true do -- Ahora, queremos uno o más blobs (CF25) seguidos del código -- para A..Z seguido de un byte 0 para convertir ACSCII a UTF-16 b,e,m = f:find(' (xCF%%[xCF%%]*[AZ])x00',e+1) -- salir cuando no haya más coincidencias si no es b entonces romper el final -- CMD.EXE no puede imprimir blobs, así que los convertimos a estrellas. -- Para ahorrar espacio, suprimimos las coincidencias sucesivas si m ~= p y luego print(string.format('%08X: %s',b,m:gsub('xCF%%','*'))) p = m final final

punto_img

Información más reciente

punto_img