En noviembre, el paquete npm event-stream fue explotado a través de una dependencia maliciosa, flatmap-stream. Toda la experiencia fue escrita aquí y el objetivo de esta publicación es utilizarla como un caso de estudio para la ingeniería inversa de JavaScript. Las tres cargas útiles asociadas con flatmap-stream son lo suficientemente simples como para que sea fácil escribir sobre ellas y lo suficientemente complejas como para que sean interesantes. Si bien no es fundamental comprender la historia de fondo de este incidente para comprender esta publicación, haré suposiciones que pueden no ser obvias si no está familiarizado con los detalles.
La ingeniería inversa de la mayoría de los códigos JavaScript es más sencilla que la de los ejecutables binarios que puede ejecutar en su sistema operativo de escritorio (después de todo, el código fuente está frente a usted), pero el código JavaScript que está diseñado para ser difícil de entender a menudo pasa por algunos pasos de ofuscación para ocultar su intención. Parte de esta ofuscación proviene de lo que se llama “minificación”, que es el proceso de reducir el recuento total de bytes de su fuente tanto como sea posible con el fin de ahorrar espacio. Esto implica acortar las variables a identificadores de un solo carácter y traducir expresiones como true a algo más corto pero equivalente como !0. La minificación es algo exclusivo del ecosistema de JavaScript debido a sus orígenes en el navegador web y, ocasionalmente, se observa en paquetes de nodos debido a la reutilización de herramientas y no está destinada a ser una medida de seguridad. Para revertir de forma básica las técnicas comunes de minimización y ofuscación, consulte la herramienta desminificar de Shape. Los pases de ofuscación dedicados pueden provenir de herramientas diseñadas para ofuscar o pueden ser realizados manualmente por el desarrollador.
El primer paso es conseguir la fuente aislada para analizarla. El paquete flatmap-stream fue diseñado específicamente para parecer inocente excepto por una payload maliciosos incluida en una sola versión del paquete, la versión 0.1.1. Puede ver rápidamente los cambios en la fuente comparando la versión 0.1.2 y la versión 0.1.1 o incluso alternando entre las URL en dos pestañas. Durante el resto de la publicación, nos referiremos a la fuente adjunta como carga útil A. A continuación, se muestra la fuente formateada de la carga útil A.
Lo primero es lo primero: NUNCA EJECUTE CÓDIGO MALICIOSO (excepto en entornos aislados). He escrito mis propias herramientas para ayudarme a refactorizar el código dinámicamente usando el conjunto de analizadores Shift y transformadores de JavaScript, pero puedes usar un IDE como Visual Studio Code para seguir esta publicación.
Al realizar ingeniería inversa de JavaScript, es valioso mantener el malabarismo mental al mínimo. Esto significa deshacerse de cualquier expresión o declaración que no agregue valor inmediato y también revertir el estado DRY de cualquier código que haya sido optimizado de manera automática o manual. Dado que analizamos estáticamente JavaScript y hacemos un seguimiento de su ejecución en nuestras cabezas, cuanto más profunda sea nuestra pila mental, más probabilidades hay de que nos perdamos.
Una de las cosas más sencillas que puedes hacer es desminificar las variables a las que se les asignan propiedades globales como require y process, como en las líneas 3 y 4.
Puedes hacer esto con cualquier IDE que ofrezca capacidades de refactorización (generalmente presionando “F2” sobre un identificador que quieras renombrar). Después de eso, vemos una definición de función, e, que parece simplemente decodificar una cadena hexadecimal.
La primera línea de código interesante parece importar un archivo que proviene del resultado de la función e decodificando la cadena "2e2f746573742f64617461".
Es extremadamente común que JavaScript deliberadamente ofuscado oscurezca cualquier valor de cadena literal para que cualquiera que eche un vistazo rápido no sea alertado por cadenas o propiedades particularmente siniestras a la vista. La mayoría de los desarrolladores reconocen que este es un obstáculo muy bajo, por lo que a menudo encontrará codificaciones que se pueden deshacer de manera trivial y en este caso no es diferente. La función e simplemente invierte cadenas hexadecimales y puedes hacerlo manualmente a través de una herramienta en línea o con tu propia función conveniente. Incluso si está seguro de comprender lo que hace la función e, sigue siendo una buena idea no ejecutarla (incluso si la extrae) con la entrada que se encuentra en un archivo malicioso porque no tiene garantías de que el atacante no haya encontrado una vulnerabilidad de seguridad que se active por los datos.
Después de invertir esa cadena, vemos que el script incluye un archivo de datos, './test/data', que se encuentra en el paquete npm distribuido.
Después de cambiar el nombre de n a datos y desofuscar las llamadas a e(n[2]) a e(n[9]), comenzamos a ver una mejor imagen de lo que estamos tratando aquí.
También es fácil ver por qué se ocultaron estas cadenas: encontrar cualquier referencia al descifrado en una biblioteca de mapas planos simple sería una clara señal de que algo anda muy mal.
Desde aquí vemos que el script está importando la biblioteca “crypto” de node.js y, después de buscar las API , encontramos que el segundo argumento para createDecipher, o aquí, es la contraseña utilizada para descifrar. Ahora podemos renombrar ese argumento y los siguientes valores de retorno con nombres sensatos según la API. Cada vez que encontremos una nueva pieza del rompecabezas, es importante inmortalizarla mediante una refactorización o un comentario, incluso si se trata de una variable renombrada que parece trivial. Es muy común que, al navegar por código extranjero durante horas, pierdas el rumbo, te distraigas o tengas que retroceder debido a alguna refactorización errónea. Usar git para guardar puntos de control durante una refactorización también es valioso, pero dejaré esa decisión en tus manos. El código ahora se ve así, con la función e eliminada porque ya no se usa junto con la declaración if (!o) {... porque no agrega valor al análisis.
También notarás que he cambiado el nombre de f a newModuleInstance. Con un código tan corto no es crítico, pero con un código que puede tener cientos de líneas es importante que todo sea lo más claro posible.
Ahora la carga útil A está en gran parte desofuscada y podemos recorrerla para entender lo que hace.
La línea 3 importa nuestros datos externos.
La línea 4 obtiene una contraseña del entorno. process.env le permite acceder a variables desde dentro de un script de nodo y npm_package_description es una variable que npm, el administrador de paquetes de nodo, establece cuando ejecuta scripts definidos en un archivo package.json.
La línea 5 crea una instancia de descifrado con el valor de npm_package_description como contraseña. Esto significa que la carga útil cifrada solo se puede descifrar cuando este script se ejecuta a través de npm y se ejecuta para un proyecto particular que tiene, en su package.json, un campo de descripción específico. Eso va a ser difícil.
Las líneas 6 y 7 descifran el primer elemento de nuestro archivo externo y lo almacenan en la variable “descifrado”.
Las líneas 8 a 11 crean un nuevo módulo y luego introducen los datos descifrados en el método no documentado _compile. Luego, este módulo exporta el segundo elemento de nuestro archivo de datos externos. module.exports es el mecanismo del nodo para exponer datos de un módulo a otro, por lo que newModuleInstance.exports(data[1]) expone una segunda carga útil cifrada que se encuentra en nuestro archivo de datos externos.
En este punto, tenemos datos cifrados que solo se pueden descifrar con una contraseña que se encuentra en un package.json en algún lugar y cuyos datos descifrados se introducen en el método _compile. Ahora nos queda un problema: ¿cómo descifrar datos cuando la contraseña es desconocida? Esta no es una pregunta trivial, si fuera fácil forzar el cifrado aes256 entonces tendríamos más problemas que con un paquete npm tomando control. Afortunadamente, no estamos tratando con un conjunto completamente desconocido de posibles contraseñas, sino con cualquier cadena que se haya ingresado en un package.json en algún lugar . Los archivos package.json se originaron como el formato de archivo para los metadatos de los paquetes npm, por lo que podemos comenzar en el registro oficial de npm. Afortunadamente, existe un paquete npm que nos proporciona un flujo de todos los metadatos del paquete .
No hay garantía de que nuestro archivo de destino esté ubicado en un paquete npm, muchos proyectos que no son npm usan package.json para almacenar la configuración de las herramientas basadas en nodos, y las descripciones de package.json pueden cambiar de una versión a otra, pero es un buen lugar para comenzar. Es posible descifrar esta carga útil con múltiples claves, lo que genera un galimatías confuso, por lo que necesitamos alguna forma de validar nuestra carga útil descifrada durante este proceso de fuerza bruta. Dado que estamos tratando con algo que se alimenta a Module.prototype._compile , que a su vez alimenta a vm.runInThisContext , podemos asumir razonablemente que la salida es JavaScript y podemos usar cualquier cantidad de analizadores de JavaScript para validar los datos. Si nuestra contraseña falla o si tiene éxito pero nuestro analizador arroja un error, entonces debemos pasar al siguiente paquete.json. Convenientemente, Shape Security ha creado su propio conjunto de analizadores de JavaScript para su uso en entornos JavaScript y Java. El script de fuerza bruta utilizado está aquí:
Después de ejecutar esto durante 92,1 segundos y procesar 740543 paquetes, obtenemos nuestra contraseña: "A Secure Bitcoin Wallet", que decodifica con éxito la carga útil incluida a continuación:
Esto fue suerte. Lo que podría haber sido un monstruoso problema de fuerza bruta terminó necesitando menos de un millón de iteraciones. El paquete afectado con la clave en cuestión terminó siendo la aplicação cliente de la billetera bitcoin Copay. Las siguientes dos cargas útiles profundizan en la aplicação en sí y, dado que la aplicação de destino se centra en el almacenamiento de bitcoins, probablemente puedas adivinar a dónde podría dirigirse esto.
Si te parecen interesantes temas como este y quieres leer un análisis de las otras dos cargas útiles o ataques futuros, asegúrate de darle “Me gusta” a esta publicación o házmelo saber en Twitter a @jsoverson .