明示的なパニックはソース コード内で簡単に識別できますが、暗黙的なパニックは開発者によって無視される可能性が高くなります。暗黙的なパニックは通常、標準またはサードパーティのライブラリによって提供される API を使用するときに発生します。開発者は API ドキュメントをよく読んで理解する必要があります。そうしないと、Rust プログラムが予期せず停止する可能性があります。
API は注意して使用してください。暗黙的なパニックは通常、標準ライブラリまたはサードパーティ ライブラリによって提供される API の誤用が原因で発生します。したがって、API を完全に理解し、潜在的なエラーを適切に処理する方法を学ぶことが重要です。開発者は、API が失敗する可能性があることを常に想定し、そのような状況に備える必要があります。
適切なエラー処理: パニックに頼るのではなく、エラー処理に Result タイプと Option タイプを使用します。前者は、エラーや特殊なケースをより制御された方法で処理します。
This page may contain third-party content, which is provided for information purposes only (not representations/warranties) and should not be considered as an endorsement of its views by Gate, nor as financial or professional advice. See Disclaimer for details.
RPC のクラッシュ: メモリセーフ ブロックチェーン RPC ノードにおける新しいタイプの脆弱性の分析
CertiK の Skyfall チームは最近、Aptos、StarCoin、Sui を含むいくつかのブロックチェーンの Rust ベースの RPC ノードに複数の脆弱性を発見しました。 RPC ノードは dApp と基盤となるブロックチェーンを接続する重要なインフラストラクチャ コンポーネントであるため、シームレスな運用にはその堅牢性が重要です。ブロックチェーン設計者は、安定した RPC サービスの重要性を認識しているため、RPC ノードを破壊する可能性のある一般的な脆弱性を回避するために、Rust などのメモリセーフな言語を採用しています。
Rust などのメモリセーフな言語を採用すると、RPC ノードがメモリ破損の脆弱性に基づく多くの攻撃を回避できます。しかし、最近の監査を通じて、メモリセーフな Rust 実装であっても、注意深く設計され、精査されていない場合、RPC サービスの可用性を妨害する可能性のある特定のセキュリティの脅威に対して脆弱になる可能性があることがわかりました。
この記事では、一連の脆弱性の発見について実践事例を交えて紹介します。
ブロックチェーン RPC ノードの役割
ブロックチェーンのリモート プロシージャ コール (RPC) サービスは、レイヤー 1 ブロックチェーンのコア インフラストラクチャ コンポーネントです。ユーザーに重要な API フロントエンドを提供し、バックエンド ブロックチェーン ネットワークへのゲートウェイとして機能します。ただし、ブロックチェーン RPC サービスは、認証なしでユーザー対話を容易にするという点で従来の RPC サービスとは異なります。サービスの継続的な可用性は非常に重要であり、サービスが中断されると、基盤となるブロックチェーンの可用性に重大な影響を与える可能性があります。
監査の観点: 従来の RPC サーバー VS ブロックチェーン RPC サーバー
従来の RPC サーバーの監査は、入力検証、認可/認証、クロスサイト リクエスト フォージェリ/サーバーサイド リクエスト フォージェリ (CSRF/SSRF)、インジェクションの脆弱性 (SQL インジェクション、コマンド インジェクションなど)、情報漏洩に主に焦点を当てています。
ただし、ブロックチェーン RPC サーバーの場合は状況が異なります。トランザクションが署名されている限り、要求元のクライアントを RPC 層で認証する必要はありません。ブロックチェーンのフロントエンドとしての RPC サービスの主な目標の 1 つは、その可用性を保証することです。失敗すると、ユーザーはブロックチェーンを操作できなくなり、オンチェーン データのクエリ、トランザクションの送信、またはコントラクト機能の発行ができなくなります。
したがって、ブロックチェーン RPC サーバーの最も脆弱な側面は「可用性」です。サーバーがダウンすると、ユーザーはブロックチェーンを操作できなくなります。さらに深刻なのは、一部の攻撃がチェーン上に広がり、多数のノードに影響を与え、さらにはネットワーク全体の麻痺につながる可能性があることです。
新しいブロックチェーンがメモリセーフ RPC を使用する理由
Aptos や Sui などの一部のよく知られたレイヤー 1 ブロックチェーンは、メモリセーフ プログラミング言語 Rust を使用して RPC サービスを実装しています。 Rust の強力な安全性と厳格なコンパイル時チェックのおかげで、Rust はスタック オーバーフローなどのメモリ破損の脆弱性、ヌル ポインタの逆参照や解放後の再参照の脆弱性に対してプログラムを実質的に免れます。
コードベースのセキュリティをさらに高めるために、開発者は安全でないコードを導入しないなどのベスト プラクティスに厳密に従います。ソース コードで #![forbid(unsafe_code)] を使用して、安全でないコードがブロックされ、フィルタリングされるようにします。
Rustプログラミング実践を実装するブロックチェーン開発者の例
整数のオーバーフローを防ぐために、開発者は通常、単純な加算と減算 (+、-) の代わりに、checked_add、checked_sub、saturating_add、saturating_sub などの関数を使用します。適切なタイムアウト、リクエスト サイズ制限、リクエスト項目制限を設定することで、リソースの枯渇を軽減します。
レイヤー 1 ブロックチェーンにおけるメモリ安全性 RPC の脅威
RPC ノードは、従来の意味でのメモリ不安に対して脆弱ではありませんが、攻撃者によって簡単に操作される入力にさらされます。メモリセーフな RPC 実装では、サービス拒否につながる可能性のある状況がいくつかあります。たとえば、メモリの増幅によりサービスのメモリが使い果たされる可能性があり、ロジックの問題により無限ループが発生する可能性があります。さらに、競合状態は、同時操作で予期しない一連のイベントが発生し、システムが未定義の状態になるという脅威を引き起こす可能性があります。さらに、依存関係やサードパーティのライブラリが不適切に管理されていると、システムに未知の脆弱性が導入される可能性があります。
この投稿の目的は、Rust のランタイム保護がトリガーされてサービス自体が異常終了する、より即時的な方法に注意を向けることです。
明示的な Rust Panic: RPC サービスを直接終了する方法
開発者は、意図的または非意図的に、明示的なパニック コードを導入する可能性があります。これらのコードは主に、予期しない状況や例外的な状況を処理するために使用されます。一般的な例としては次のようなものがあります。
assert!(): 条件を満たす必要がある場合にこのマクロを使用します。アサートされた条件が失敗すると、プログラムはパニックを起こし、コードに重大なエラーがあることを示します。
Panic!(): この関数は、プログラムが回復できず続行できないエラーに遭遇したときに呼び出されます。
unreachable!(): コードの一部を実行すべきでない場合にこのマクロを使用します。このマクロが呼び出された場合は、重大な論理エラーが発生していることを示します。
unimplemented!() および todo!(): これらのマクロは、未実装の機能のプレースホルダーです。この値に達すると、プログラムはクラッシュします。
unwrap(): このメソッドは、オプションまたは結果タイプに使用されます。Err 変数または None が発生すると、プログラムはクラッシュします。
脆弱性 1: Move Verifier でアサートをトリガーします!
Aptos ブロックチェーンは、Move バイトコード検証ツールを使用して、バイトコードの抽象解釈を通じて参照セキュリティ分析を実行します。 ute() 関数は TransferFunctions トレイトの実装の一部であり、基本ブロックでのバイトコード命令の実行をシミュレートします。
関数 ute_inner() のタスクは、現在のバイトコード命令を解釈し、それに応じて状態を更新することです。基本ブロックの最後の命令まで実行した場合 (index == last_index で示される)、関数はassert!(self.stack.is_empty()) を呼び出してスタックが空であることを確認します。この動作の背後にある目的は、すべての操作のバランスが取れていることを保証することです。これは、すべてのプッシュに対応するポップがあることも意味します。
通常の実行フローでは、抽象解釈中にスタックのバランスが常に保たれます。これは、バイトコードを解釈する前に検証する Stack Balance Checker によって保証されます。しかし、抽象インタプリタの領域まで視野を広げると、スタックバランスの仮定が常に有効であるとは限らないことがわかります。
AbstractInterpreter のanalyze_functionの脆弱性に対するパッチ
その中核となる抽象インタープリタは、基本ブロック レベルでバイトコードをエミュレートします。元の実装では、ute_block 中にエラーが発生すると、分析プロセスがエラーをログに記録し、制御フロー グラフ内の次のブロックに実行を続行するように指示されます。これにより、実行ブロック内のエラーによりスタックのバランスが崩れる状況が生じる可能性があります。この場合に実行が継続されると、スタックが空でない場合にassert! チェックが行われ、パニックが発生します。
これにより、攻撃者が悪用する機会が与えられます。攻撃者は、ute_block() で特定のバイトコードを設計することでエラーをトリガーすることができ、スタックが空でない場合に ute() がアサートを実行し、アサート チェックが失敗する可能性があります。これにより、さらにパニックが発生し、RPC サービスが終了し、可用性に影響します。
これを防ぐために、実装された修正により、ute_block 関数で初めてエラーが発生したときに解析プロセス全体が確実に停止されるようになり、エラーによるスタックの不均衡により解析を継続するときに発生する可能性のあるその後のクラッシュのリスクが回避されます。 。この変更により、パニックを引き起こす可能性のある条件が削除され、抽象インタープリタの堅牢性と安全性が向上します。
** 脆弱性 2: StarCoin でパニックを引き起こします! **
Starcoin ブロックチェーンには、Move 実装の独自のフォークがあります。この Move リポジトリでは、Struct 型のコンストラクターでパニックが発生しています! 提供された StructDefinition にネイティブ フィールド情報がある場合、パニックは明示的にトリガーされます。 。
正規化ルーチンでの初期化された構造の明示的なパニック
この潜在的なリスクは、モジュールを再配布するプロセスに存在します。公開されたモジュールがデータ ストアにすでに存在する場合、既存のモジュールと攻撃者が制御する入力モジュールの両方にモジュールの正規化が必要です。このプロセス中に、「normalized::Module::new」関数が攻撃者が制御する入力モジュールからモジュール構造を構築し、「パニック!」を引き起こします。
正規化ルーチンの前提条件
このパニックは、特別に作成されたペイロードをクライアントから送信することによってトリガーされる可能性があります。したがって、悪意のある攻撃者が RPC サービスの可用性を妨害する可能性があります。
構造体初期化パニックパッチ
Starcoin パッチでは、ネイティブのケースを処理するための新しい動作が導入されています。今では、パニックになるのではなく、空の ec を返します。これにより、ユーザーがデータを送信してパニックが発生する可能性が軽減されます。
暗黙的な Rust Panic: RPC サービスを終了する見落とされやすい方法
明示的なパニックはソース コード内で簡単に識別できますが、暗黙的なパニックは開発者によって無視される可能性が高くなります。暗黙的なパニックは通常、標準またはサードパーティのライブラリによって提供される API を使用するときに発生します。開発者は API ドキュメントをよく読んで理解する必要があります。そうしないと、Rust プログラムが予期せず停止する可能性があります。
BTreeMap での暗黙的なパニック
Rust STD の BTreeMap を例に挙げてみましょう。 BTreeMap は、ソートされたバイナリ ツリーでキーと値のペアを編成する、一般的に使用されるデータ構造です。 BTreeMap は、キーによって値を取得するための 2 つのメソッド、get(&self, key: &Q) とindex(&self, key: &Q) を提供します。
メソッド get(&self, key: &Q) は、キーを使用して値を取得し、Option を返します。オプションは Some(&V) で、キーが存在する場合は値の参照を返し、BTreeMap でキーが見つからない場合は None を返します。
一方、index(&self, key: &Q) は、キーに対応する値への参照を直接返します。ただし、これには大きなリスクが伴います。BTreeMap にキーが存在しない場合、暗黙的なパニックが引き起こされます。適切に処理しないと、プログラムが予期せずクラッシュする可能性があり、潜在的な脆弱性となります。
実際、index(&self, key: &Q) メソッドは std::ops::Index 特性の基礎となる実装です。この特性は、不変コンテキスト (つまり、コンテナー) でのインデックス操作です。 [index] ) は便利な糖衣構文を提供します。開発者は btree_map を直接使用できます [key] 、index(&self, key: &Q) メソッドを呼び出します。ただし、この使用法ではキーが見つからない場合にパニックが発生する可能性があり、プログラムの安定性に対する暗黙の脅威となる可能性があるという事実を無視している可能性があります。
脆弱性 3:Sui RPC で暗黙的なパニックを引き起こす
Sui モジュール リリース ルーチンを使用すると、ユーザーは RPC 経由でモジュール ペイロードを送信できます。 RPC ハンドラーは、SuiCommand::Publish 関数を使用して、バイトコード検証のためにバックエンド検証ネットワークにリクエストを転送する前に、受信したモジュールを直接逆アセンブルします。
この逆アセンブリ中に、送信されたモジュールの code_unit セクションを使用して VMControlFlowGraph が構築されます。ビルド プロセスは、「blocks」という名前の BTreeMap に保存される基本ブロックの作成で構成されます。このプロセスにはマップの作成と操作が含まれており、特定の条件下で暗黙的なパニックがトリガーされます。
簡略化したコードは次のとおりです。
VMControlFlowGraph 作成時の暗黙的なパニック
そのコードでは、コードを実行し、コード単位ごとに新しい基本ブロックを作成することによって、新しい VMControlFlowGraph が作成されます。基本ブロックは、BTreeMap という名前のブロックに保存されます。
ブロック マップは、ENTRY_BLOCK_ID で初期化されたスタックを反復するループ内で block[&block] を使用してインデックス付けされます。ここでは、ブロック マップ内に少なくとも 1 つの ENTRY_BLOCK_ID があることを前提としています。
ただし、この仮定が常に成り立つわけではありません。たとえば、コミットされたコードが空の場合、「基本ブロックの作成」プロセス後も「ブロック マップ」は空のままです。後でコードが &blocks[&block].successors の for succ を使用してブロック マップを走査しようとすると、キーが見つからないと暗黙的なパニックが発生する可能性があります。これは、blocks[&block] 式は本質的に、index() メソッドの呼び出しであり、前述したように、BTreeMap にキーが存在しない場合にパニックが発生するためです。
リモート アクセスを持つ攻撃者は、空の code_unit フィールドを含む不正なモジュール ペイロードを送信することで、この関数の脆弱性を悪用する可能性があります。この単純な RPC リクエストは、JSON-RPC プロセス全体をクラッシュさせます。攻撃者が最小限の労力でこのような不正なペイロードを送信し続けると、サービスが継続的に中断されることになります。ブロックチェーン ネットワークでは、これはネットワークが新しいトランザクションを確認できなくなり、サービス妨害 (DoS) 状況が発生する可能性があることを意味します。ネットワーク機能とシステムに対するユーザーの信頼は重大な影響を受けます。
スイの修正: RPC 発行ルーチンから逆アセンブリを削除
Move Bytecode Verifier の CodeUnitVerifier は、code_unit セクションが空にならないようにする責任があることに注意してください。ただし、操作の順序により、RPC ハンドラーが潜在的な脆弱性にさらされます。これは、検証プロセスが、RPC が入力モジュールを処理した後の段階である Validator ノードで行われるためです。
この問題に対応して、Sui はモジュールのリリース RPC ルーチン内の逆アセンブリ関数を削除することで脆弱性を解決しました。これは、RPC サービスが潜在的に危険な未検証のバイトコードを処理するのを防ぐ効果的な方法です。
また、オブジェクト ルックアップに関連する他の RPC メソッドにも逆アセンブリ機能が含まれていますが、空のコード セルの使用に対して脆弱ではないことにも注意してください。これは、既存の公開モジュールを常にクエリして逆アセンブルしているためです。公開されたモジュールは検証されている必要があるため、VMControlFlowGraph を構築するときは空ではないコード セルの前提が常に当てはまります。
開発者への提案
開発者は、明示的および暗黙的なパニックによるブロックチェーンの RPC サービスの安定性に対する脅威を理解した後、これらのリスクを防止または軽減する戦略を習得する必要があります。これらの戦略により、計画外のサービス停止の可能性が減り、システムの復元力が向上します。したがって、CertiK の専門家チームは次の提案を提案し、Rust プログラミングのベスト プラクティスとしてリストします。
Rust パニックの抽象化: 可能な限り、Rust の catch_unwind 関数を使用してパニックを捕捉し、エラー メッセージに変換することを検討してください。これにより、プログラム全体のクラッシュが防止され、開発者は制御された方法でエラーを処理できるようになります。
API は注意して使用してください。暗黙的なパニックは通常、標準ライブラリまたはサードパーティ ライブラリによって提供される API の誤用が原因で発生します。したがって、API を完全に理解し、潜在的なエラーを適切に処理する方法を学ぶことが重要です。開発者は、API が失敗する可能性があることを常に想定し、そのような状況に備える必要があります。
適切なエラー処理: パニックに頼るのではなく、エラー処理に Result タイプと Option タイプを使用します。前者は、エラーや特殊なケースをより制御された方法で処理します。
ドキュメントとコメントを追加する: コードが十分にドキュメント化されていることを確認し、重要なセクション (パニックが発生する可能性のあるセクションを含む) にコメントを追加します。これは、他の開発者が潜在的なリスクを理解し、効果的に対処するのに役立ちます。
要約
Rust ベースの RPC ノードは、Aptos、StarCoin、Sui などのブロックチェーン システムで重要な役割を果たします。これらは DApp と基盤となるブロックチェーンを接続するために使用されるため、その信頼性はブロックチェーン システムのスムーズな動作にとって非常に重要です。これらのシステムはメモリセーフな言語 Rust を使用していますが、設計が不十分になるリスクは依然としてあります。 CertiK の研究チームは、メモリセーフ プログラミングにおいて注意深く慎重な設計が必要であることを実証する実例を用いて、これらのリスクを調査しました。