EthereumのVanityアドレスツールの脆弱性により約230億円が流出

こんにちは!弐号です。

仮想通貨のマーケット・メーカであるWintermuteがVanityアドレス作成ツールであるProfanityを利用したところ、その脆弱性を突かれて約$160M(約230億円)が流出したようです。

参考記事:230億円ハッキングのWintermute、最新調査を公開 | Profanityが原因か

ここではその技術的な詳細に迫ります。

Vanityアドレスとは?

Vanityアドレスとは、アドレスの一部に好きな文字列を含めることで人間が識別しやすくしたアドレスのことです。

Vanityアドレスを生成するツールはBitcoin用やEthereum用などを含めて多数出回っており、それなりに利用しているユーザがいます。

かくいう私も、寄付用のBitcoinアドレスとして "1viriaLNKJ9nuwbEi7zpxNEbCnSbUMM3j" というアドレス(先頭がTwitter名の "virial" になっている)を持っています。

このようにVanityアドレスを利用すると、対外的に公開するアドレスについてはその所有者が明確になり、送金先を間違えるといった事態を減らすことができますので、それなりに利用価値があります。

またEthereumなどでは先頭にゼロがたくさんついているアドレスについてはガス代(トランザクション手数料)が多少安くなるという話もあり、今回ハッキングを受けたWintermuteもガス代の節約のために先頭にゼロがたくさんついているアドレスを生成して利用していたようです。

Vanityアドレスの生成方法

Vanityアドレスの生成方法としてはいろいろな方法が考えられますが、一般的にはランダムな秘密鍵からスタートし、秘密鍵をインクリメントしながら対応するアドレスを計算していき、アドレスが目的の形をしていたら終了する、という手続きを行うものが多いと思います。

この処理は大量に秘密鍵を生成し、アドレスを計算していくという力技で生成されるため、多くの場合にはGPUを利用して計算の高速化を図っています。

実際、Wintermuteの利用していたProfanityというVanityアドレス生成ツールは、OpenCLというGPU専用(CPUでも使えなくはないですが、ややこしいのでここではこう表現しています)の言語で書かれたツールになっており、GPUを用いることで非常に高速に目的のアドレスを生成することができます。

実際にProfanityを動かし、先頭にゼロがたくさん並ぶアドレスを作ってみると、以下のように数十秒程度で生成ができます。

$ ./profanity.x64 --leading 0
Mode: leading
Target: Address
Devices:
  GPU0: NVIDIA GeForce RTX 3080 Ti, 12636061696 bytes available, 80 compute units (precompiled = yes)

Initializing OpenCL...
  Creating context...OK
  Loading kernel from binary...OK
  Building program...OK

Initializing devices...
  This should take less than a minute. The number of objects initialized on each
  device is equal to inverse-size * inverse-multiple. To lower
  initialization time (and memory footprint) I suggest lowering the
  inverse-multiple first. You can do this via the -I switch. Do note that
  this might negatively impact your performance.

  GPU0 initialized

Initialization time: 1 seconds
Running...
  Always verify that a private key generated by this program corresponds to the
  public key printed by importing it to a wallet of your choice. This program
  like any software might contain bugs and it does by design cut corners to
  improve overall performance.

  Time:     1s Score:  5 Private: 0x801b3e29aa105b9453c21403870994215d4ab7ea2912c654f1b2ed57925deaaf Address: 0x00000b6d1d7695bd50b8f49085e3eb24ad81faf5
  Time:     1s Score:  6 Private: 0x801b3e29a9ea001453c21403870994215d4ab7ea2912c654f1b2ed57925deab0 Address: 0x000000f914f48f30c1d5224a29f624f2a4bc5751
  Time:     2s Score:  7 Private: 0x801b3e29a9f8735953c21403870994215d4ab7ea2912c654f1b2ed57925deadd Address: 0x0000000d621cdcf4cf3ebf0bd9b809c50b102460
  Time:    15s Score:  8 Private: 0x801b3e29a9eadcc953c21403870994215d4ab7ea2912c654f1b2ed57925df139 Address: 0x000000006ccebf69d3790afcab2da8a9d959942e
Total: 572.392 MH/s - GPU0: 572.392 MH/s

Profanityの脆弱性

それでは、今回のProfanityの脆弱性はどこにあるのでしょうか?

それについては公式GitHubリポジトリのこちらのイシューを読むと分かります。

具体的には Dispatcher.cpp100〜121行目を見てみましょう。

cl_ulong4 Dispatcher::Device::createSeed() {
#ifdef PROFANITY_DEBUG
    cl_ulong4 r;
    r.s[0] = 1;
    r.s[1] = 1;
    r.s[2] = 1;
    r.s[3] = 1;
    return r;
#else
    // Randomize private keys
    std::random_device rd;
    std::mt19937_64 eng(rd());
    std::uniform_int_distribution<cl_ulong> distr;

    cl_ulong4 r;
    r.s[0] = distr(eng);
    r.s[1] = distr(eng);
    r.s[2] = distr(eng);
    r.s[3] = distr(eng);
    return r;
#endif
}

前半の #ifdef マクロの中身はデバッグ時にのみ有効となる、テスト用のコードですので読み飛ばしてもらって構いません。

注目すべきは

    std::random_device rd;
    std::mt19937_64 eng(rd());
    std::uniform_int_distribution<cl_ulong> distr;

という部分です。

C++言語の詳細には立ち入りませんが、この処理は64ビット版のメルセンヌ・ツイスタと呼ばれる疑似乱数生成器を初期化する処理です。

この疑似乱数生成器へのシード値としては32ビットの整数値を渡しています。

ところで秘密鍵は256ビットの整数値でした。

すなわち、本来あるべきビット数よりも遥かに短いシード値を使って初期化をしてしまっています。

これでは、考えうるシード値をすべて試すことで、秘密鍵が復元されてしまう可能性があります!

すべてのシード値に対してGPUにて1秒間の計算を行うと仮定したとしても、必要な計算時間は

\frac{2^{32}}{60 \times 60 \times 24} = 49,710 \text{日}

程度ですので、これをAWSなどのクラウドインスタンスで1,000GPUを用いて並列計算すれば、50日程度で解析ができてしまいます!

おそらく攻撃者は、この脆弱性に気づき、実際にAWSなどのクラウドを用いて解析を行うことで秘密鍵を特定したのだと思われます。

これを防ぐためには、メルセンヌ・ツイスタなどの疑似乱数生成器は使わずに、初期値となる秘密鍵をきちんと256ビット分、暗号学的に安全な乱数エントロピーを用いて初期化しないといけません。

しかし残念ながら、Profanityのソースコードではそうなっていなかったため、Profanityを用いて生成されたアドレスは脆弱性を抱えることになってしまったのです。

この脆弱性の報告を受け、Profanityの作者は、GitHubリポジトリをアーカイブし、さらにソースコードに意図的にコンパイルができないような細工をして、一般のユーザが間違って使ってしまわないようにしたようですが、不幸にもWintermute社は脆弱性のあるアドレスを使い続けてしまったようです。

教訓としては、信頼できないVanityアドレス生成ツールは使わないようにしたほうがいいですね。

もっというと、どんなに気を使っていたとしてもVanityアドレスは本質的に256ビットのアドレス空間の中から極めて少ないアドレス空間を切り出す行為になってしまっていますので、そもそもVanityアドレスを大金を扱うアドレスとして利用するのは控えたほうがよいでしょう。

アドレスに入っている金額が少なければ、GPUを借りて解析を行うコストがペイしなくなるため、Vanityアドレスに大金を入れないというのは比較的まっとうな対策と言えます。

できれば、LedgerやTREZORなどの実績のあるハードウェアウォレットを用いて、信頼できる方法で秘密鍵を初期化することが求められます。

みなさんも、こうしたハッキング被害に合わないように注意しましょうね!!!

まとめ

  • WintermuteはProfanityと呼ばれるVanityアドレス生成ツールを使って生成したアドレスを使っていた
  • Profanityにはアドレス生成部分に致命的な脆弱性があり、これにより生成されたアドレスは安全ではなかった
  • 秘密鍵(アドレス)の生成には信頼できる方法を用いよう!

以上になります。

ちょっと技術的な内容が多かったのですが、クリプトを安全に利用するためにはこうした技術的な知識は避けて通れない部分ではありますので、ぜひこれを機会に理解できるように努めてくださいね!

ではでは!

関連記事