Fugas de información en JVM Heap Dumps – Extracción de claves privadas
17/11/2015 -
Abundantes son las prácticas que se implementan en el mundo empresarial para prevenir fugas de información. Estas van des del control de malware, pasando por las auditorías, análisis de riesgos y terminando en procesos de prevención de escape involuntario de datos. Un factor muy importante es la custodia y tratamiento de ficheros digitales, en este artículo nos centraremos en los volcados de memoria (aka Heap Dumps) de las JVMs.
Por un lado, numerosos son los escenarios y derivados en los que entra en juego la extracción de un Java Heap Dump:
- Problemas de Memory leaks
- Situaciones de crash de la JVM, de manera adicional a los coredumps o crash dumps.
- Estudios de rendimiento.
- Resolución de problemas varios: OOME por heap, problemas de TLA, GC overhead, swap, análisis “post-mortem, etc.
por otra parte, muchos son los episodios en los que la insuficiencia de prudencia y prominente descuido hacen que dichos ficheros:
- Se distribuyan a terceros sin un procedimiento firme del uso de la información. En ocasiones, en los procesos de gestión de problemas frente a incidentes recurrentes dentro de los marcos enumerados se asignan las tareas de análisis a terceros proporcionando los heap dumps sin ser totalmente conscientes de la información implícita que se está cediendo.
- Se pierdan entre máquinas. ¿Cuántos heap dumps no hemos encontrado en directorios intermedios, de backup, temporales (que luego se quedan allí), e incluso en directorios destinados a la rotación de logs? ¿Cuántos en máquinas “multiuso” porque el entorno donde hemos generado el dump es productivo y no debemos “malgastar” recursos en tareas costosas de análisis y que luego allí se han quedado? ¿Cuántos en los portátiles nominales? (y un largo etcétera).
Para darle el énfasis necesario a la materia y concienciación, vamos a realizar una prueba de concepto, en un escenario de laboratorio, ficticio, controlado, pero extrapolable; extrayendo uno de los datos digitales más privados y preservados que podría tener una organización (su clave privada), a partir de un heap dump sacado de una JVM que corre un WebLogic Server como servidor de aplicaciones.
PoC
Buscamos el proceso java y extraemos un heap dump.
Lo portamos a local (si es necesario) para depurar mejor.
Arrancamos el jhat.
Accedemos al servidor HTTP levantado para proceder con el análisis del heap dump. Cargamos el histograma del heap.
Si consultamos la Java Cryptography Architecture (JCA) Reference Guide vemos que en Java disponemos de la lista de interfaces siguientes para implementar las claves:
Partiendo de WebLogic como servidor de aplicaciones, nos interesa la implementación en RSA.
Al ser interfaces buscamos en el histograma por sus implementaciones, “Impl” por convención de nombres, y obtenemos:
De todos los tipos posibles encontramos en el heap dump dos instancias de la clase RSAPrivateCrtKeyImpl. Si consultamos la API de la openJDK (suficiente como referencia), nos indica que es la implementación de una clave privada RSA con CRT.
Si regresamos al histograma del heap dump y analizamos las instancias de RSAPrivateCrtKeyImpl, vemos que su superclase es PKCS8Key:
Si buscamos PKCS8Key en el codigo fuente de la openJDK vemos que se puede utilizar para la implementación de claves privadas. El atributo que llama la atención es el “key”:
Se describe como los bytes de la clave sin la información del algoritmo, pero nosotros lo conocemos de antemano porque venimos de su subclase 😉
Si continuamos analizando el heap, descartamos todas las subclases (no nos interesan) y nos situamos en la instancia de interés RSA, vemos que sus miembros de la instancia son:
Efectivamente el atributo “key” es heredado de su superclase, con contenido:
El formato, tal como se ha visto en el código fuente, es un array de bytes, en este caso en sistema hexadecimal.
Para obtener la clave he creado una pequeña clase Java para hacer la transformación. El objetivo no es ni la refactorización de código, manejo de excepciones, rendimiento, ni ningún otro que no sea la obtención de la clave.
A partir del array de bytes extraído del heap dump, y contemplando que el rango de bytes en Java es de [-128, 127] deberemos hacer un “cast” de los valores hexadecimales que representen números negativos para que la compilación sea correcta. Debido al gran tamaño del array, lo más sencillo es aplicar masivamente el cast a todos los valores (con el eclipse, sed, etc). Con este paso obtendremos la clave privada en formato DER (binario). Aprovechamos el código para transformarla en formato PEM (ASCII) por portabilidad, y ser más instructivo.
Código fuente aquí.
Si ejecutamos la aplicación obtenemos la clave privada:
Si la comprobamos con OpenSSL:
Habremos obtenido la clave privada.
Otra manera más laboriosa pero muy interesante de obtener la clave privada, es construir su especificación a partir de los miembros RSA. Si desempolvamos un poco nuestras matemáticas discretas y recapitulamos con el enlace de cómo calcular la clave RSA, lo contrastamos con los atributos propios (no heredados) de la clase RSAPrivateCrtKeyImpl:
nos percatamos de que tenemos a nuestra disposición, en el heap dump, todos los valores necesarios:
Los miembros son del tipo BigInteger, que según código fuente de la implementación interna de la JDK:
Nosotros ya disponemos de los valores del BigInteger a partir del heap dump, y el valor más interesante es el atributo “mag” como array de enteros:
Podemos usar los valores coeff, d, e, n, p, pe, q, qe para crear una especificación de la clave RSA a partir de ellos. La clase BigInteger tiene declarado el atributo “mag” como final, por lo que no podrá ser modificado más allá del constructor. Por definición, para crear el BigInteger necesitamos (entre otras posibilidades peores para el caso) de un array de bytes en complemento a2 en binario de la representación del BigInteger y nosotros partimos de la tabla de enteros sacada del heap dump. Para ahorrarnos conversiones, cálculos matemáticos y aumentar las probabilidades de error, nos las arreglaremos para aprovechar la clase interna BigInteger de la JDK. Para ello no contemplaré la posibilidad de extenderla (que lo permite), pues se deberían implementar métodos y no solucionaría nada a priori, pues el valor de mag sólo lo podemos modificar a partir de la superclase. Además, estamos tratando con una clase inmutable.
Para solucionar esta encrucijada me apoyaré con el uso de la reflexión, con ella haremos un “hook” a la clase interna BigInteger de la JDK para modificar el atributo “mag” y realizar un “by-pass” a su lógica de instanciación. En primer momento, inicializaremos con un valor positivo cualquiera, para ahorrar tener que usar también la reflexión al atributo “signum”, ya que en criptografía de infraestructura de clave pública, asumimos que los números son enteros (en todo caso podemos ver todos los signum en el heap dump), ya que por definición al utilizar números primos como aquellos que pueden dividirse entre ellos mismos y la unidad, es decir, tienen dos divisores; caso que no se cumple con números negativos.
Creamos otra clase de Java para realizar las tareas. Uso de reflexión:
Para la implementación aprovechamos un trozo de código de los atributos de la propia JDK:
Por lo que:
Instanciaremos el BigInteger por el motivo comentado anteriormente y crearemos las tablas de enteros con los valores que vayamos sacando del heap dump hasta tener los ocho valores:
Vamos asignando los atributos de la clase mientras modificamos (hook al BigInteger) el valor del BigInteger a cada paso:
Ahora creamos la clave privada a partir de su especificación:
Para terminar, transformamos la clave privada a formato PEM:
Para obtener la clave RSA:
Código fuente aquí.
Hemos obtenido la clave privada de la organización a partir del heap dump de la JVM del servidor de aplicaciones. No se ha necesitado ninguna clave de acceso para obtenerla, ya que es el WebLogic Server el que la extrae del keystore con la configuración de contraseña proporcionada y la mantiene en memoria.
No es necesaria tener demasiada imaginación para percatarnos del problema que puede conllevar obtener (o ceder) la clave privada. Hay que ser precavido de la información que puede contener un “simple” dump de memoria y actuar de manera proporcional.