Ingeniería inversa de JavaScript mediante ejemplos

Ecosistema de F5 | 02 de enero de 2019

carga útil A de flatmap-stream

En noviembre, el paquete npm event-stream fue atacado mediante una dependencia maliciosa, flatmap-stream. Todo lo ocurrido se explicó aquí y el objetivo de esta publicación es utilizarlo como caso práctico de ingeniería inversa de JavaScript. Las tres cargas útiles asociadas a flatmap-stream son lo bastante sencillas como para poder explicarlas fácilmente y lo bastante complejas como para resultar interesantes. Aunque no es imprescindible conocer los antecedentes de este incidente para entender esta publicación, daré por sentadas algunas cosas que quizá no resulten obvias si no se conocen hasta cierto punto los detalles.

La ingeniería inversa de la mayoría del código JavaScript es más sencilla que la de los ejecutables binarios que se pueden ejecutar en un sistema operativo de escritorio —al fin y al cabo, el código fuente está justo delante—, pero el código JavaScript diseñado para ser difícil de entender suele someterse a varias fases de ofuscación para ocultar su propósito. Parte de esta ofuscación procede de lo que se denomina «minificación», que consiste en reducir al máximo el número total de bytes del código fuente para ahorrar espacio. Esto implica acortar las variables a identificadores de un solo carácter y transformar expresiones como true en otras más cortas pero equivalentes, como !0. La minificación es prácticamente exclusiva del ecosistema de JavaScript debido a sus orígenes en los navegadores web; en ocasiones también aparece en paquetes de node por la reutilización de herramientas, y no pretende ser una medida de seguridad. Para revertir de forma básica las técnicas habituales de minificación y ofuscación, se puede utilizar la herramienta unminify de Shape. Las fases específicas de ofuscación pueden proceder de herramientas diseñadas para ofuscar el código o ser realizadas manualmente por el desarrollador.

El primer paso es hacerse con el código fuente aislado para analizarlo. El paquete flatmap-stream se creó específicamente para parecer inofensivo, salvo por una carga maliciosa incluida únicamente en una versión del paquete, la versión 0.1.1. Puedes ver rápidamente los cambios en el código 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. En el resto de la publicación, nos referiremos al código fuente añadido como carga A. A continuación se muestra el código fuente formateado de la carga A.

Ingeniería inversa mediante un ejemplo, imagen 1

Lo primero de todo: NUNCA EJECUTES CÓDIGO MALICIOSO (excepto en entornos aislados). He creado mis propias herramientas para poder refactorizar código de forma dinámica mediante el conjunto de analizadores sintácticos y transformadores de JavaScript de Shift; no obstante, para seguir los pasos de este artículo puedes utilizar un IDE como Visual Studio Code.

Al aplicar ingeniería inversa a JavaScript, conviene reducir al mínimo el esfuerzo mental. Esto implica eliminar cualquier expresión o sentencia que no aporte valor inmediato, así como deshacer la aplicación del principio DRY en cualquier código que se haya optimizado de forma automática o manual. Dado que estamos analizando JavaScript de forma estática y siguiendo mentalmente su ejecución, cuanto más crezca nuestra pila mental, más probabilidades tendremos de perdernos.

Una de las cosas más sencillas que puedes hacer es desminificar las variables a las que se asignan propiedades globales como require y process, como ocurre en las líneas 3 y 4.

Ingeniería inversa mediante ejemplos, imagen 2

Puedes hacerlo con cualquier IDE que ofrezca funciones de refactorización (normalmente, pulsando «F2» sobre el identificador que quieras renombrar). A continuación, vemos la definición de una función, e, que parece limitarse a decodificar una cadena hexadecimal.

Ingeniería inversa mediante ejemplos, imagen 3

La primera línea de código interesante parece importar un archivo cuya ruta se obtiene al descodificar la cadena "2e2f746573742f64617461" mediante la función e.

Ingeniería inversa mediante ejemplos, imagen 4

Es muy habitual que el código JavaScript ofuscado deliberadamente oculte cualquier valor de cadena literal para evitar que alguien que le eche un vistazo se alarme al ver cadenas o propiedades especialmente inquietantes a simple vista. La mayoría de los desarrolladores saben que este obstáculo es muy fácil de superar, por lo que es frecuente encontrar codificaciones que se pueden revertir sin dificultad, y este caso no es una excepción. La función e simplemente invierte las cadenas hexadecimales, algo que puede hacerse manualmente mediante una herramienta en línea o con una función auxiliar propia. Aunque tengas la certeza de entender lo que hace la función e, sigue siendo recomendable no ejecutarla (ni siquiera después de extraerla) con datos procedentes de un archivo malicioso, ya que nada garantiza que el atacante no haya descubierto una vulnerabilidad de seguridad que se active con esos datos.

Tras invertir esa cadena, vemos que el script incluye un archivo de datos, './test/data', que se encuentra en el paquete npm distribuido.

Ingeniería inversa mediante ejemplos, imagen 5

Después de cambiar el nombre de n a data y desofuscar las llamadas de e(n[2]) a e(n[9]), empezamos a hacernos una idea más clara de a qué nos enfrentamos.

Imagen 6 de Ingeniería inversa mediante ejemplos

También es fácil entender por qué se ocultaron estas cadenas: encontrar cualquier referencia al descifrado en una simple biblioteca de flatmap sería una señal inequívoca de que algo va muy mal.

A partir de aquí vemos que el script está importando la biblioteca «crypto» de node.js y, tras consultar las API, descubrimos que el segundo argumento de createDecipher, o en este caso, es la contraseña utilizada para descifrar. Ahora podemos cambiar el nombre de ese argumento y de los siguientes valores de retorno por nombres descriptivos basados en la API. Cada vez que encontramos una nueva pieza del rompecabezas, es importante dejarla plasmada mediante una refactorización o un comentario, aunque solo se trate de cambiar el nombre de una variable y parezca algo trivial. Es muy habitual que, tras pasar horas examinando código ajeno, pierdas el hilo, te distraigas o tengas que volver atrás debido a alguna refactorización errónea. También resulta útil usar git para guardar puntos de control durante una refactorización, pero dejo esa decisión en tus manos. El código queda ahora como se muestra a continuación, tras eliminar la función e porque ya no se utiliza, así como la sentencia if (!o) {..., ya que no aporta nada al análisis.

Ingeniería inversa mediante ejemplos, imagen 7

También observarás que he cambiado el nombre de f a newModuleInstance. En un código tan corto no es crucial, pero en uno que podría tener cientos de líneas es importante que todo quede lo más claro posible.

Ahora, la carga útil A está en gran medida desofuscada y podemos analizarla paso a paso para entender qué hace.

La línea 3 importa nuestros datos externos.

Imagen 8 de Ingeniería inversa mediante ejemplos

La línea 4 obtiene una contraseña del entorno. process.env permite acceder a las variables desde un script de Node y npm_package_description es una variable que npm, el gestor de paquetes de Node, establece al ejecutar scripts definidos en un archivo package.json.

Ingeniería inversa mediante ejemplos, imagen 9

La línea 5 crea una instancia de descifrado usando como contraseña el valor de npm_package_description. Esto significa que la carga útil cifrada solo puede descifrarse cuando este script se ejecuta mediante npm y para un proyecto concreto que tenga un campo de descripción específico en su archivo package.json. No va a ser fácil.

Ingeniería inversa mediante ejemplos, imagen 10

Las líneas 6 y 7 descifran el primer elemento de nuestro archivo externo y lo almacenan en la variable «decrypted»

Imagen 11 de Ingeniería inversa mediante ejemplos

Las líneas 8-11 crean un nuevo módulo y, a continuación, pasan los datos descifrados al método no documentado _compile. Después, este módulo exporta el segundo elemento de nuestro archivo de datos externo. module.exports es el mecanismo de Node 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 externo.

Ingeniería inversa mediante ejemplos, imagen 12

Llegados a este punto, tenemos datos cifrados que solo pueden descifrarse con una contraseña incluida en algún package.json y que, una vez descifrados, se pasan al método _compile. Ahora nos encontramos con un problema: ¿cómo se descifran unos datos cuando se desconoce la contraseña? No es una cuestión trivial; si fuera fácil descifrar por fuerza bruta el cifrado aes256, tendríamos problemas más graves que el secuestro de un paquete npm. Por suerte, no nos enfrentamos a un conjunto de posibles contraseñas completamente desconocido, sino simplemente a cualquier cadena que se haya introducido en un package.json en algún lugar. Los archivos package.json surgieron como formato para los metadatos de los paquetes npm, así que podemos empezar por el registro oficial de npm. Por suerte, existe un paquete npm que nos proporciona un flujo con todos los metadatos de los paquetes.

Ingeniería inversa mediante ejemplos, imagen 13

No hay ninguna garantía de que nuestro archivo objetivo se encuentre en un paquete npm; muchos proyectos que no son de npm utilizan package.json para almacenar la configuración de herramientas basadas en Node, y las descripciones de package.json pueden cambiar de una versión a otra, pero es un buen punto de partida. Es posible descifrar esta carga útil con varias claves y obtener como resultado texto ininteligible, por lo que necesitamos alguna forma de validar la carga útil descifrada durante este proceso de fuerza bruta. Dado que estamos tratando con algo que se pasa a Module.prototype._compile que, a su vez, se lo pasa a vm.runInThisContext podemos suponer razonablemente que el resultado es JavaScript y utilizar cualquiera de los numerosos analizadores de JavaScript para validar los datos. Si nuestra contraseña falla, o si funciona pero el analizador genera un error, debemos pasar al siguiente package.json. Afortunadamente, Shape Security ha creado su propio conjunto de analizadores de JavaScript para utilizarlos en entornos JavaScript y Java. El script de fuerza bruta utilizado está aquí:

Ingeniería inversa mediante ejemplos, imagen 14

Tras ejecutarlo durante 92,1 segundos y procesar 740543 paquetes, obtenemos nuestra contraseña —«Un monedero de Bitcoin seguro»—, que descodifica correctamente la carga útil incluida a continuación:

Ingeniería inversa mediante ejemplos, imagen 15

Tuvimos suerte. Lo que podría haber sido un problema monstruoso de fuerza bruta acabó requiriendo menos de un millón de iteraciones. El paquete afectado que contenía la clave en cuestión resultó ser la aplicación cliente del monedero de bitcoin Copay. Las dos cargas útiles siguientes profundizan en la propia aplicación y, dado que la aplicación objetivo está diseñada para almacenar bitcoins, probablemente ya puedas imaginar por dónde van los tiros.

Si te parecen interesantes temas como este y quieres leer un análisis de los otros dos payloads o de futuros ataques, no olvides darle a «Me gusta» a esta publicación o hacérmelo saber en Twitter en @jsoverson.

Compartir
Etiquetas: 2021

Artículos relacionados del blog

Cree servicios públicos seguros, resilientes y preparados para la IA con F5 y AWS
Ecosistema de F5 | 26/05/2026

Cree servicios públicos seguros, resilientes y preparados para la IA con F5 y AWS

Descubre cómo las organizaciones del sector público pueden modernizar de forma segura sus aplicaciones y soluciones de IA, al tiempo que gestionan los sistemas heredados, el cumplimiento normativo y el riesgo operativo.

Protección de los sistemas de IA: por qué la seguridad debe avanzar al ritmo de la innovación
Ecosistema de F5 | 04/08/2025

Protección de los sistemas de IA: por qué la seguridad debe avanzar al ritmo de la innovación

Desde acelerar avances revolucionarios en medicina y educación hasta transformar la vida cotidiana, la IA está cambiando nuestra forma de vivir y trabajar. Pero también ofrece grandes oportunidades a los ciberdelincuentes.

Avanzar hacia la optimización digital tras la transformación
Ecosistema de F5 | 06/08/2024

Avanzar hacia la optimización digital tras la transformación

Descubre estrategias de optimización digital para garantizar políticas de seguridad coherentes, impulsar la madurez de la transformación digital y superar los retos de la transformación.

Proyecto de código abierto destacado: mayor observabilidad con F5 Cloud Logger iApp
Ecosistema de F5 | 07/06/2018

Proyecto de código abierto destacado: mayor observabilidad con F5 Cloud Logger iApp

En el punto de mira del código abierto: cómo aumentar la observabilidad. Lee el blog de F5 para descubrir cómo exportar fácilmente la telemetría de BIG-IP a plataformas modernas de análisis en la nube.

El estado de la entrega de aplicaciones en 2018: la transformación digital impulsa la automatización y la multinube pese a las preocupaciones de seguridad
Ecosistema de F5 | 16/01/2018

El estado de la entrega de aplicaciones en 2018: la transformación digital impulsa la automatización y la multinube pese a las preocupaciones de seguridad

Descubre las principales conclusiones del cuarto informe anual de F5 sobre el estado de la entrega de aplicaciones.

Estado de la entrega de aplicaciones en 2017: automatización, API y DevOps
Ecosistema de F5 | 17/04/2017

Estado de la entrega de aplicaciones en 2017: automatización, API y DevOps

Descubre el informe sobre el estado de la entrega de aplicaciones de 2017. Lee el blog retrospectivo de F5 sobre los inicios del auge de las redes declarativas y los flujos de DevOps.