Update 2023/3/30

2022-10-25 事後分析

strapi-blog-api-image

背景について

10月13日に、 Toshi さんが Discord#sdk-js チャンネルにおいて、とある予期せぬ挙動の報告をしました。彼は非推奨の TypeScript SDK と新しい JavaScript SDK を比較したところ、トランザクションハッシュの計算が異なっていることに気づきました。これは想定していなかった挙動であり、さらに気になった点は、そのどちらもがネットワークに受け入れられている(そして確認されている)ということでした。

根本的な原因

チームはふたつの重大なバグを即座に特定いたしました。 - ひとつはCatapult (the Symbol client)に、もうひとつはTypeScript及びJavaのSDKでした。

Clientについて

Catapultは "Fushicho 2" (2019年11月8日) において、アグリゲートコンプリーテッドとアグリゲートボンデッドのTransactionsHash フィールドを検証することを目的として、アグリゲートトランザクションプラグインに新しいバリデータの追加がされました。ですが見落としがあったため、アグリゲートトランザクションプラグインに登録されずに、その結果呼び出されることがありませんでした。
manager.addStatelessValidatorHook([config](auto& builder) {
        // the following line should have been present but was not
    builder.add(validators::CreateAggregateTransactionsHashValidator()); 

    builder.add(validators::CreateBasicAggregateCosignaturesValidator(
			config.MaxTransactionsPerAggregate,
			config.MaxCosignaturesPerAggregate));
	if (config.EnableStrictCosignatureCheck)
		builder.add(validators::CreateStrictAggregateCosignaturesValidator());
});
ローンチを急いだため、 Failure_Aggregate_Transactions_Hash_Mismatch の失敗コードを発動するE2Eテストの追加を失念していました。その結果、Symbolはアグリゲートトランザクションのハッシュの検証を適切に経ないままでローンチを迎えておりました。
さらに、非推奨のTypeScript、Javaの SDK、とPython、JavaScriptのSDKの間で、アグリゲートトランザクションのハッシュ計算に相違がありました。
PythonとJavaScriptのSDKはトランザクションのハッシュを正しく計算していましたが、今年の初めに非推奨といたしましたTypeScriptとJavaのSDKはそうではありませんでした。

SDKについて

非推奨のTypeScript SDKには、その計算に2つのバグがありました。
まず、アグリゲートトランザクションはトランザクションを埋め込むコンテナであることを思い出してください。それぞれのトランザクションの埋め込みは8バイトを境にスタートすることが保証されています。これを実現するために、ゼロにされたパッドバイトが、必要なときにいつでもトランザクション間に挿入されます。埋め込みトランザクションのハッシュを計算する際には、トランザクションの意味のあるデータのみをハッシュ化し、ゼロパッドバイトは除外する必要があります。残念ながら、TypeScript SDKはこのゼロパッドバイトをハッシュの計算に含めていたのです。そのことは不本意なことでしたが、これによってセキュリティが低下することはありません。
本当のバグは、Merkleハッシュの計算において発見されました。spliceの不適切な使用により、spliceが要素を置き換えるのではなく、挿入してしまったのです。2番目のパラメータは置換のために 1 であるべきだったのです。
hashes.splice(i / 2, 0, this.hash([hashes[i], hashes[i + 1]]));
テスト網羅率が低かっただけではなく、私たちの標準的な監査を用いていなかったために、これらの過失はどちらとも発見されていませんでした。
非推奨のJava SDKには、TypeScript SDKと同じMerkleハッシュの計算バグがありましたが、驚いたことに埋め込みトランザクションハッシュは(パディングなしで)正しく計算されていました。
この2つのSDKが何年も前から異なるアグリゲートトランザクションハッシュを計算していたにもかかわらず、今まで誰も気づかなかったというのは少々興味深い話ですが、少なくとも共通の監査を用いる必要がありました。

想定される攻撃方法

アグリゲートトランザクションのハッシュのチェックができないため、連帯署名人はアグリゲートトランザクションのヘッダーにのみ署名していました。そこには組み込みトランザクションの合計サイズがヘッダーのpayload_size と一致しなければならないという制約があるのみでした。
TypeScriptとJavaのSDKで使用されていた無効なMerkle Hashアルゴリズムのバグによって、破損したMerkle Hashはトランザクションのサブセットを書き換えから保護するだけでした。例えば、3つのトランザクションがある場合、最初の2つのトランザクションだけが書き換えから保護され、3つ目は保護されなかったのです。3つ目のトランザクションのサイズが変更されない限り、内容を何にでも書き換えることができたのです。

攻撃例:アグリゲートコンプリート

モザイク交換をしようとしているAlice、 Bob、Charlieの3人がいるとします。
AliceはBobとCharlieにそれぞれ10アルパカモザイクを支払いたいと考えています。 それと引き換えに、Charlieは100XYMを支払うことに同意しています。
Aliceは以下のような3つの部分からなるアグリゲートコンプリートトランザクションを作成します。
  1. AliceがBobに10アルパカを送る
  2. AliceがCharlie10アルパカを送る
  3. CharlieがAliceに100XYM送る
トランザクションを作成した後、Aliceはそれに署名します。彼女はそれをBobに渡し、Bobはそれに連署します。BobはそれをCharlieに渡します。CharlieはAliceを欺こうと思っています。
Charlieはこのバグを知っていて、3つ目のトランザクションを書き換え、代わりにアリスに0XYMを送るようにします。あるいは、もっと悪いことに、Aliceが自分に100XYMを送るように書き換えることもできたのです!
3つ目のトランザクションを書き換えた後、Charlieはそれに署名し、ネットワークに送信します。CharlieとBobはそれぞれAliceから10アルパカを受け取ります。Aliceは何も受け取らず、もしくは結果としてXYMをチャーリーに送ってしまっていたかもしれないのです。

攻撃例:アグリゲートボンデッド

前述した攻撃例は、アグリゲートボンデッドトランザクションに対しても実行することができました。
トランザクションを作成した後に、Aliceはそれをネットワークに送り、そこで部分取引キャッシュに追加されます。Bobは自分の連帯署名をネットワークに送信します。
この時点で、Charlieがそのトランザクションをダウンロードします。彼は前述した方法と同様に、3つ目のトランザクションを書き換えます。次に、彼はBobの連帯署名をネットワークからダウンロードし、それを添付します。最後に、彼は自分自身で署名し、アグリゲートをネットワークに送信します。

評価について

発見後私たちは、悪意ある者がこのバグを利用する前にネットワークにパッチを適用することと、現在のチェーンにおける被害を評価することの2点を優先しました。
分析時点(10月25日)では、ネットワーク上で38,8607のアグリゲートが確認されました。

グループ1

  • 73,615 (19.07%)は正しい Merkleハッシュアルゴリズムとともに、パディングされていないハッシュを使用していました。
  • 129,987 (33.68%)は正しいMerkleハッシュアルゴリズムとともに、パディングされたトランザクションハッシュを使用していました。これらはTypeScript SDKで開始された可能性が高いです。Merkleハッシュアルゴリズムのバグの仕様上、2つ以下のアグリゲートを含むアグリゲートは、暗号的に検証可能なハッシュを計算します。

グループ2

  • 184,471 (47.80%)は、無効なMerkleハッシュアルゴリズムとともに、パディングされたトランザクションハッシュを使用していました。これらはTypeScript SDKで開始された可能性が高いです。 これらのうち:
    • 102,591はキーリンク取引(単一所有アカウントによる発行)
  • 514 (0.13%)は、無効なMerkleハッシュアルゴリズムとともに、パディングされていないトランザクションハッシュを使用していました。これらはJava SDKで開始された可能性が高いです
  • 20 (0.005%)は破損しており、上記のいずれかの方法でも計算されていませんでした。さらに分析すると、これらはすべて雑多なスクリプトによって開始されたようです。これらのスクリプトを見直すと、それらのいくつかは、アグリゲートトランザクションハッシュを計算した後に、1つまたは複数の組み込みトランザクションの変更をしていましたが、再計算は行っていませんでした。
Group 1の組み込みトランザクションは、すべてSymbolの設計で意図されたとおりに暗号的に検証することができます。 Group 2 の組み込みトランザクションは、どれも暗号的に検証することはできません。幸いなことに、いったんそれらが確定すると、ステートハッシュが一致しなければならないので、攻撃者はそれらを偽造することができなくなります。

まとめ

Catapult

v1.0.3.4では、 AggregateTransactionsHashValidatorがアグリゲートトランザクションプラグインに正しく登録されるようになりました。
フォークブロックの 1'690'500以降では、すべてのアグリゲートトランザクションハッシュはパディングなしで、正しいMerkleハッシュアルゴリズムで、埋め込まれたトランザクションハッシュから計算されます。
フォークブロックに先立つ、以下のいずれかの条件を満たすトランザクションハッシュを持つアグリゲートトランザクションを許可します。:
  • 正しい Merkle Hash アルゴリズムを持つ、パディングされていないトランザクションハッシュ
  • 正しい Merkle Hash アルゴリズムを持つ、 パディングされたトランザクションハッシュ(2つ以下のTS SDK埋め込みトランザクション)
  • 無効なMerkle Hashアルゴリズムを持つ、パディングされていないトランザクションハッシュ(2つ以上のJava SDK埋め込みトランザクション)
  • 無効なMerkle Hashアルゴリズムを持つ、パディングされたトランザクションハッシュ(2つ以上のTS SDK埋め込みトランザクション)
  • ハッシュが新しいcorrupt_aggregate_transaction_hashes ネットワーク構成に一致する項目を持つトランザクション(20件)
パフォーマンス上の理由から、フォークブロック以降では、バージョン2のアグリゲートトランザクションのみが許可されます。このバージョンアップにより、アグリゲートトランザクションのハッシュバリデータはステートレスになり、メインコミットロックの外側で並列に実行されるようになります。

SDK

PythonとJavaScriptのSDKが更新され、V2アグリゲートトランザクション (コンプリートとボンデッド) がサポートされました。
TypeScript SDKがアップデートされ、正しいMerkleハッシュアルゴリズムの使用、パディングなしの埋め込みトランザクションハッシュの計算、V2アグリゲートトランザクションの作成が可能になり、V1アグリゲートトランザクションの読み取りと検証が限定的にサポートされるようになりました。
Java SDKが更新され、正しいMerkleハッシュアルゴリズムが使用され、V2アグリゲートトランザクションが限定的にサポートされるようになりました。

前へ向かって

クライアント開発者としての課題のひとつは、ネットワークの脆弱性をどう扱うかです。通知するユーザーが増えれば、その分攻撃にさらされる危険性が増しますので、常に責任ある情報公開と分散化のトレードオフが行われているのです。公にそれを話せば、日和見主義的なハッカーと イタチごっこ ゲームを繰り返すとになるでしょうし、非公開で話すと、ユーザーベースの大部分を孤立させることになります。
標準的なフレームワークがあるわけではありません。 その時々に応じて 対処プランを立てるわけです。
ですがネットワークの更新を迅速に行うためのより良い方法が必要なことは明らかです。
従来、プロジェクトにはバリデータやサービスプロバイダーが集まる「war room」がありますが、そこでは開発者や他の事業者と交流することはほとんどなく、情報発信の負担が大きくなっています。今後は情報発信を迅速に行うために「war room」を設置し、コミュニティとして、チャンネル外に情報を漏らさない、流さないという紳士協定を結んで、お互いに責任を持つべきでしょう。
また、より良いリリースの方法が必要なことも明らかです。今年初めにその負担を減らすためmonorepo構造に移行しましたが(Jenkinsに感謝)、フォークから発表までに要した時間は 約8時間 でした。これは受け入れがたいことです。
そして、私たちは適切な開発スピードを保つ必要があります。任意の期限に間に合わせるためにSymbolのリリースを急ぐあまり、E2Eのテストが省略され、重要な監査も省略されました。製品全体のテスト網羅率は低いままです。また、Bootstrapの最後のバグにより、一部のノードがアップグレードできない事態が発生し、この事後分析発表時においても処置にあたっているところです。
私たちはノード運用者の負担を軽減し、将来的にシームレスなアップグレードを実現するために、フォーク後のノード展開とそのメンテナンスのためのツールに取り組む予定です。私たちのチームは、デスクトップウォレットとモバイルウォレットの両方のテスト網羅率を向上させて、TypeScript SDKから新しいデュアルNEMおよびSymbol JavaScript SDKへの移行に取り組んでいます。また、将来的には未定義または不正な挙動を、観察力のあるコミュニティ開発者と熱心なコミュニティメンバーの双方が確実に発見できるように、ドキュメントのオーバーホールに取り組んでいます。
最後に、 私たちはバグの発見をしてくれた Toshi さんに日本円で約¥370,000の懸賞金を授与することをお伝えします。 親愛なる宇宙ライオンのおかげで、Symbolネットワークに安全が取り戻されました。

News
Community
Docs
Contact