https://medium.com/numen-cyber-labs/the-story-of-a-high-vulnerability-in-move-reference-safety-verify-module-2340f3d8c642
0x0 序文
少し前に、重大な問題が見つかりました脆弱性 アプトス Movevm で。綿密な調査の結果、整数オーバーフローである別の重大な脆弱性が発見されましたが、今回は非常に興味深いものです。
Move 言語は、バイトコードを実行する前にコード単位を検証することがわかっています。コード ユニットで、4 つのステップに分割されていることを確認します。このバグは、reference_safety で発生します。これについては、以下で詳しく説明します。
このモジュールは、手続き本体の参照安全性を検証するための伝達関数を定義します。チェックには、ダングリング参照がないこと、変更可能な参照へのアクセスが安全であること、およびグローバル ストレージ参照へのアクセスが安全であることの検証が含まれます (ただし、これらに限定されません)。
これは検証エントリポイントで、analyze_function を呼び出します。
analyze_function では、基本ブロックごとに関数を検証しますが、基本ブロックとは何ですか?
の中にコンパイラの構築 、基本ブロックは、入口を除いて分岐がなく、出口を除いて分岐がない直線的なコードシーケンスです。
Move 言語では、基本ブロックをどのように識別しますか?
Move 言語では、基本ブロックは、バイトコードをトラバースし、すべての分岐命令とループ命令を見つけることによって決定されます。以下のコアコードを確認できます。
Move 言語のコード ブロックの例を次に示します。 3 つの基本ブロックがあることがわかります。分岐は命令によって決定されます: BrTrue、Branch、Ret。
移動中の 0x1 参照安全性
Move は、次の 2 種類の参照をサポートしています。不変 — & で定義(例: &T) および可変 — &mut (例: &mut T).不変 (&) 参照を使用して構造体からデータを読み取り、可変 (&mut) を使用してそれらを変更できます。適切なタイプの参照を使用することで、セキュリティの維持に役立ちます。また、このメソッドが値を変更するのか、読み取りのみを行うのかを知っておく必要があります。
以下は、公式の Move チュートリアルの例です。
上記の例では、mut_ref_t が値 t の変更可能な参照であることがわかります。
そのため、Move 参照安全モジュールは、関数を単位として、関数内の基本ブロックをスキャンし、バイトコード命令の検証を通じてすべての参照操作が正当であるかどうかを判断することによって、参照が有効かどうかを確認しようとします。
次の図は、参照の安全性を検証するルーチンを示しています。
ここでの状態は、参照を保護するために使用されるボロー グラフとローカルを含む AbstractState です。
借りる_グラフ ローカル参照間の関係を表すグラフです。
上の図からわかるように、前の状態 これにはローカルとボロー グラフ (L ,BG) が含まれており、基本ブロックを実行すると、投稿状態 (L',BG') を使用して、前後の状態をマージしてブロックの状態を更新し、このブロックの事後条件を後続のブロックに伝播します。これは節の海 V8 ターボファンの最適化。
以下のコードは、上の図に対応するメイン ループです。まず、ブロック コードを実行し (命令の実行が失敗した場合は AnalysisError を返します)、次にブロック コードのマージを試みます。前の状態 と投稿状態 かどうかを判断することによって結合結果 変更されるかどうか。それが変更され、現在のブロックにエッジポイントが含まれている場合 (つまり、ループがあることを意味します)、ループの先頭に戻り、次のラウンドでこのブロックが実行されるまで実行されます。投稿状態 に等しい前の状態 または何らかのエラーによって中止されます。
joinResult が変更されたか変更されていないかをどのように判断しますか?
上記のコードにより、ローカルと借用関係の関係が変化したかどうかを判断することで、結合結果が変化したかどうかを知ることができます。ここでの join_ 関数は、ローカルおよび借用グラフの状態を更新するために使用されます。
以下の join_ コードでは、6 行目で新しいローカル マップを初期化します。9 行目は、すべての値が None の場合にローカルのすべてのインデックスを反復するために使用されます。ブロックを実行する前と後に新しいブロックに挿入しないようにします。地元の地図。ステートが値を持つ前にポスト ステートが None の場合、brow_graph id を解放する必要があります。これは、値の借用関係を排除することを意味します。反対も同じです。特に、両方の値が存在し、同じである場合は、30 行目から 33 行目のように新しいマップにそれらを挿入し、38 行目でborrow_graph をマージします。
上記から、self.iter_locals() がローカルの数であることがわかります。このローカルには、関数の実際のローカルだけでなく、パラメーターも含まれていることに注意してください。
0x2 脆弱性
ここで、脆弱性に関連するすべてのコードをクロスしました。見つかりましたか?
脆弱性が見つからない場合、それは問題ではありません。以下では、脆弱性を引き起こすプロセスについて詳しく説明します。
パラメータの長さが256を超えるローカルの長さを追加する場合、ブローコードの最初に問題はないようです。
しかし、この関数は項目タイプ u8 の Iterator を返します。
そのため、関数 join_() では function_view.parameters().len() と function_view.locals().len() の合計値が 256 を超えています。
コードでは、self.iter_locals() のローカル用 、ローカル変数は u8 型です。 256 回の繰り返しの後、オーバーフローが発生します。オーバーフロー後の local の値は 8 です。
実際には、Move にはローカルの番号を検証するルーチンがありますが、残念ながら、パラメーターの長さを含めずに、チェック境界モジュールでローカルのみを検証します。
開発者は、パラメーター + ローカル値を確認する必要があることをここで知っているようです。ただし、コードは、境界チェック モジュールでローカル カウントを検証します。パラメーターの長さは含まれません。
0x3 オーバーフローを DoS に移動
コード ブロックをスキャンするためのメイン ループがあり、execute_block 関数を呼び出して状態に参加した後であることがわかっています。移動コードが存在する場合、ループは再び実行を開始するブロックにジャンプします。
したがって、ループ コード ブロックを作成し、オーバーフローを利用してブロックの状態を変更すると、AbstractState オブジェクトに新しいローカル マップが作成されます。以前とは異なり、関数 execute_block 関数でブロックを再度実行すると、この関数がわかります。コード バイトコードを分析し、ローカルへのアクセスを許可するため、参照値のオフセットが新しい AbstractState ローカルのマップに存在しない場合、DoS につながります。
コードを監査した結果、MoveLoc/CopyLoc/FreeRef オペコードを使用することで、この目標を達成できることがわかりました。
ここで、ファイル パスの execute_block 関数によって呼び出されるコードである copy_loc 関数を見てみましょう。
move/language/move-bytecode-verifier/src/reference_safety/abstract_state.rs
行 287 で、コードは LocalIndex as パラメータによってローカル値を取得しようとします。LocalIndex が存在しない場合、パニックが発生します。ノードがこの悪質なコードを実行すると、Imagation がノード全体をクラッシュさせます。
0x4PoC
これは、git commit で再現できる PoC です:add615b64390ea36e377e2a575f8cb91c9466844
これはクラッシュログです:
スレッド 'regression_tests::reference_analysis::PoC' が呼び出された `Option::unwrap()` でパニックに陥りました。 「なし」で値」、言語/move-bytecode-verifier/src/reference_safety/abstract_state.rs:287:39
注: `RUST_BACKTRACE=1` で実行します。バックトレースを表示する環境変数
DoS トリガーの手順:
コード ブロックが無条件分岐であることがわかります。最後の命令 branch(0) を実行するたびに、最初の命令に戻るため、execute_block と join 関数が複数回呼び出されます。
1. 初回 ここでは、パラメーターを SignatureIndex(0) に設定します。Locals を SignatureIndex(0) にすると、num_locals は 132*2=264 になります。だから電話した後
先頭の新しいローカルの長さは 264–256=8 になります
2. 2 回目に execute_block 関数を実行し、最初の命令 copy_local(57) を実行すると、57 はスタックにプッシュする必要があるローカルのオフセットですが、今回は長さ 8 のローカルのみで、オフセット 57 は存在しません。 、したがって、これにより get(57).unwrap() 関数は何も返さず、パニックになります。
0x5 まとめ
これが、この脆弱性に関するすべての話です。それから、次のことを学びます:
まず、この脆弱性は、絶対に安全なコードが存在しないことを示しています。 Move 言語は、コードが実行される前に適切な静的検証を実行しますが、この脆弱性と同様に、オーバーフローの脆弱性によって以前の境界チェックが完全にバイパスされる可能性があります。
第二に、プログラマーは時に過失を犯す可能性があるため、コード監査は非常に重要です。 Move 言語セキュリティのリーダーとして、Move のセキュリティ問題を深く掘り下げていきます。
3 点目は、Move 言語の場合、予期しない状況が発生しないように、言語設計者がチェック コードを追加することをお勧めします。
現在、Move 言語は主に検証段階で一連のセキュリティ チェックを実行していますが、これでは不十分だと思います。検証がバイパスされると、実行段階でのセキュリティ強化が不十分になり、さらに危険が深まり、より深刻な問題が発生します。最後に、Move 言語に別の脆弱性が発見されました。これについては、近日中に公開します。