https://medium.com/@numencyberlabs/analysis-of-the-first-critical-0-day-vulnerability-of-aptos-move-vm-8c1fd6c2b98e
1. 序文
Move プログラミング言語は、イーサリアムの Solidity 言語よりも強力な利点があるため、最近人気が高まっています。 Move は、Aptos や Sui など、多くの有名なプロジェクトで使用されています。最近、Numen Web3 セキュリティ 脆弱性検出製品は、Aptos パブリック チェーンの仮想マシン (VM) で重大レベルのセキュリティの脆弱性を発見しました。私たちが発見したのは、言語の脆弱性が原因で Aptos ノードがクラッシュし、サービス拒否が引き起こされる可能性があるということでした。この記事では、この脆弱性の説明を通じて、Move 言語とそのセキュリティについての理解を深めていただければ幸いです。 Move言語のセキュリティ研究のリーダーとして、私たちはその生態学的セキュリティに継続的に貢献していきます。
2. Move 言語の重要な概念
モジュールとスクリプト
Move には、モジュールとスクリプトの 2 種類のプログラムがあります。モジュールは、構造型とそれらの型で動作する関数を定義するライブラリです。構造体型は Move のグローバル ストレージのパターンを定義し、モジュール関数はストレージを更新するためのルールを定義します。モジュール自体もグローバル ストレージに格納されます。スクリプトは、従来の言語のメイン関数と同様に、実行可能ファイルへのエントリ ポイントです。スクリプトは通常、公開されたモジュールの関数を呼び出して、グローバル ストレージを更新します。スクリプトは、グローバル ストレージに公開されない一時的なコード フラグメントです。 Move ソース ファイル (またはコンパイル ユニット) には、複数のモジュールとスクリプトが含まれる場合があります。ただし、モジュールの公開またはスクリプトの実行では、個別の仮想マシン (VM) 操作が使用されます。
オペレーティング システムに詳しい人にとって、Move モジュールはシステムの実行可能ファイルの実行時に読み込まれる動的ライブラリ モジュールに似ており、スクリプトはメイン プログラムに似ています。ユーザーは、モジュールを呼び出すコードを含む独自のスクリプトを記述して、グローバル ストレージにアクセスできます。
グローバルストレージ
Move プログラムの目的は、グローバル ストレージをツリー形式で読み書きすることです。プログラムは、ファイル システム、ネットワーク、またはこのツリーの外部にあるデータにはアクセスできません。
擬似コードでは、グローバル ストレージは次のようになります。
構造的には、グローバル ストレージはフォレストであり、アカウントのアドレスをルートとするツリーで構成されます。各アドレスには、リソース データとモジュール コードを格納できます。上記の疑似コードが示すように、各アドレスは、指定されたタイプの最大 1 つのリソース値と、指定された名前の最大 1 つのモジュールを格納できます。
MOVE 仮想マシンの原則
movevm と evm 仮想マシンは同じで、ソース コードをバイト コードにコンパイルし、仮想マシンで実行する必要があります。次の図は、プロセスを示しています。
1. バイトコードは関数 execute_script を介してロードされます
2. load_script 関数を実行します。この関数は主にバイトコードをデシリアライズするために使用され、バイトコードが正当かどうかを検証します。検証に失敗した場合は、失敗として返されます。
3. 検証が成功すると、実際のバイトコード コードが実行されます。
4.バイトコードを実行し、リソース、モジュールを含むグローバルストレージの状態にアクセスまたは変更します
注: Move に関連する機能は他にも多数ありますが、ここではすべてを紹介することはせず、Move 言語の機能をセキュリティの観点から引き続き分析します。
3. 脆弱性の説明
この脆弱性は、主に検証モジュールに関係しています。具体的な脆弱性について説明する前に、検証モジュールと StackUsageVerifier::verify の機能を紹介します。
検証モジュール
バイトコードコードを実際に実行する前に、バイトコードの検証が行われ、検証はそれぞれいくつかのサブプロセスに分割できることがわかっています。
彼らです:
境界チェッカー 、主にモジュールとスクリプトの境界セキュリティをチェックするために使用されます。これには、署名、定数などの境界のチェックが含まれます。
重複チェッカー CompiledModule の各ベクトルに異なる値が含まれているかどうかを検証するチェッカーを実装するモジュール
署名チェッカー 、関数パラメーター、ローカル変数、および構造体メンバーに署名が使用されている場合に、フィールド構造が正しいことを確認します
命令の一貫性 、命令の一貫性を検証します
定数 定数が元の型であること、および定数のデータがその型に正しくシリアル化されていることを確認するために使用されます
CodeUnitVerifier 、それぞれ stack_usage_verifier.rs および abstract_interpreter.rs を介して、関数本体コードの正確性を検証します。
script_signature 、スクリプトまたはエントリ関数が有効な署名であることを確認します
脆弱性は検証プロセス内で発生します
CodeUnitVerifier::verify_script(config, スクリプト)? ;
関数。ここには多くの検証サブプロセスがあることがわかります。
これらは、スタック セーフ チェックサム、タイプ セーフ チェックサム、ローカル変数セーフ チェックサム、リファレンス セーフ チェックサムです。この脆弱性は、スタックのセキュリティ検証プロセスで発生します。
スタック セキュリティ検証 (StackUsageVerifier::verify)
このモジュールは、関数のバイトコード命令シーケンス内の基本ブロックがバランスよく使用されていることを確認するために使用されます。 Ret (呼び出し元に戻る) オペコードで終わるブロックを除く各基本ブロックは、最初と同じスタック高さでブロックを離れることを保証する必要があります。さらに、どの基本ブロックでも、スタックの高さはブロックの先頭のスタックの高さより低くしてはなりません。
すべてのブロックをループして、上記の条件が満たされていることを確認します。
ループを反復して、すべての基本ブロックの正当性を検証します。
脆弱性の詳細
先に紹介したように、movevm はスタック仮想マシンであるため、命令の正当性を検証する場合、まず命令のバイトコードが正しいことを確認する必要があり、次にスタック メモリが正しいことを確認する必要があることは明らかです。ブロック呼び出しの後は有効です。つまり、スタックはスタック操作後にバランスが取れています。verify_block
関数は、2 番目の目的を達成するために使用されます。
からわかるようにverify_block
ブロック コード ブロック内のすべての命令をループし、スタックに対する命令ブロックの効果が正当かどうかを加算または減算して検証します。num_pops
、num_push
.まず、を通してstack_size_increment < num_pops
スタック スペースが有効かどうかを判断します。もしもnum_pops
より大きいstack_size_increment
これは、バイトコード ポップの数がスタック自体のサイズよりも大きく、エラーが返され、バイトコード チェックサムが失敗することを意味します。次に、stack_size_increment -= num_pops; stack_size_increment += num_push;
、これら 2 つの命令は、各命令の実行後にスタックの高さへの影響を変更します。そして最後に、ループが終了すると、stack_size_increment
つまり、このブロックに操作を保持した後、スタックのバランスをとる必要があります。
ここでは問題がないように見えますが、16 行のコードの実行では、整数オーバーフローが発生しているかどうかが判断されないため、大きな num_pushes、stack_size_increment を構築することで間接的に制御できる整数オーバーフローの脆弱性が発生します。 .では、このような膨大な数のプッシュをどのように構築するのでしょうか?
問題ないように見えますが、ここでは16行目のコードを実行しているため、整数オーバーフローの有無は判断していません。その結果、stack_size_increment
特大のを構築することによって間接的に制御することができますnum_push
となり、整数オーバーフローの脆弱性が発生します。
ここではまず、move バイトコード ファイル形式を紹介する必要があります。
バイトコード ファイル形式の移動
Windows PE ファイルや Linux ELF ファイルと同様に、move バイトコード ファイルは .mv で終わり、ファイル自体には特定の形式があります。
最初はマジック、値は A11CEB0B、次はバージョン情報、テーブルの数、その後はテーブル ヘッダーです。多くのテーブルが存在する可能性があります。テーブルの種類はテーブルの種類で、合計 0x10 種類 (図の右側に示されているように) です。詳細については、move 言語のドキュメントを参照してください。次は、テーブルのオフセットと長さです。テーブル。その次がテーブルの内容で、最後がSpecific Dataで、モジュールの場合はModule Specific Data、スクリプトタイプの場合はScript Specific Dataの2種類があります。
構築された悪意のあるファイル形式
ここでは、スクリプトで Aptos と対話しているため、以下に示すファイル形式を構築して、stack_size_increment オーバーフローを引き起こします。
まず、このバイトコードファイルのフォーマットを説明しましょう:
+0x00–0x03: マジック ワード 0xA11CEB0B
+0x04–0x7: ファイル形式のバージョン,そのバージョンは 4
+0x8–0x8: テーブル数、値は 1
+0x9–0x9: テーブルの種類で、そのタイプは SIGNATURES です
+0xa-0xa: テーブル オフセット、値は 0
+0xb-0xb: テーブルの長さ、値は 0x10
+0xc-0x18: SIGNATURES Tokenのデータ
0x22 から始まる、スクリプトのメイン関数コードのコード部分です。
move-disassemblyr ツールを介して、命令の逆アセンブル コードが次のようになっていることがわかります。
このうち、0、1、2 の 3 つの命令に対応するコードは、それぞれ赤枠、緑枠、黄枠のデータです。
LdU64 は、脆弱性自体とは関係ありません。ここではあまり詳しく説明しませんが、興味がある場合はコードを確認してください。ここでは、VecUnpack 命令の説明に焦点を当てます。 VecUnpack の機能は、コード内でベクター オブジェクトが検出されたときに、すべてのデータをスタックにプッシュすることです。
この構築されたファイルでは、VecUnpack を 2 回構築します。そのベクトルの数はそれぞれ 3315214543476364830,18394158839224997406 です。
関数が命令効果
が実行されると、以下のコードの 2 行目が実際に実行されます:
を実行した後、命令効果
初めて (1,3315214543476364830) を返します。このとき、stack_size_increment は 0、num_pops は 1、num_pushes は 3315214543476364830 です。2 回目の戻り値は (1,18394158839224997406) です。再実行時stack_size_increment += num_pushes ;
stack_size_increment は既に 0x2e020210021e161d (3315214543476364829) です。
num_pushes は 0xff452e02021e161e (18394158839224997406) であり、2 つを足すと u64 の最大値よりも大きくなり、データの切り捨てが発生し、stack_size_increment の値が 0x12d473012043c2c3b になり、整数オーバーフローが発生し、Aptos ノードがクラッシュします。これにより、ノードの実行が停止します。 Rust 言語のセキュリティ機能により、C/C++ のようなコード セキュリティへのさらなる影響は発生しません。
4. 脆弱性の影響
この脆弱性は Move 実行モジュールで発生するため、チェーン上のノードに対して、バイトコード コードが実行されると、DoS 攻撃が発生します。深刻なケースでは、Aptos ネットワークが完全に停止する可能性があり、これは計り知れない損害を引き起こし、ノードの安定性に深刻な影響を与えます。
5.公式修正
この脆弱性を発見したとき、公式の Aptos チームに報告したところ、すぐに脆弱性が修正されました。修正のスクリーンショットについては、下の図を参照してください。
関連するコードのリンクは次のとおりです。