この記事では、Solidity (0.8.13<=solidity<0.8.17) コンパイラのコンパイル処理において、Yul 最適化機構の欠陥により状態変数の代入操作が誤って削除されてしまう問題をソースコードレベルから詳しく解説しています。予防。
最初のレベルのタイトル
1. 脆弱性の詳細
公式文書公式文書。
コンパイル プロセスの UnusedStoreEliminator 最適化ステップで、コンパイラは「冗長な」ストレージ書き込み操作を削除しますが、「冗長性」の識別欠陥により、Yul 関数ブロックが特定のユーザー定義関数 (関数内部の分岐は呼び出し側ブロックの実行フローに影響を与えません)、Yul 関数ブロック内の呼び出された関数の前後に同じ状態変数への書き込み操作があり、これによりブロック内のユーザー定義関数が事前に Yul 最適化メカニズムによって呼び出されるストレージ書き込み操作はコンパイル レベルから完全に削除されます。
次のコードを考えてみましょう。
contract Eocene {
uint public x;
function attack() public {
x = 1;
x = 2;
}
}
UnusedStoreEliminator が最適化する場合、x=1 は関数 Attack() の実行全体にとって明らかに冗長です。当然のことながら、最適化された Yul コードは、コントラクトのガス消費量を削減するために x= 1; を削除します。
次に、カスタム関数への呼び出しを途中に挿入することを検討します。
contract Eocene {
uint public x;
function attack(uint i) public {
x = 1;
y(i);
x = 2;
}
function y(uint i) internal{
if (i > 0){
return;
}
assembly { return( 0, 0) }
}
}
明らかに、y() 関数の呼び出しにより、y() 関数が関数実行フロー全体を終了させる可能性がある場合、y() 関数が関数 Attack() の実行に影響を与えるかどうかを判断する必要があります (これはロールバックではないことに注意してください (Yul コードの return() 関数)、その場合、x= 1 は明らかに削除できません。そのため、上記のコントラクトでは、y() 関数にアセンブリ {return(0, 0)} が存在します。メッセージ呼び出し全体が終了する可能性があるため、x= 1 は当然削除できません。
しかし、Solidity コンパイラでは、コード ロジックの問題により、コンパイル中に x= 1 が誤って削除され、コード ロジックが永久に変更されてしまいました。
実際のコンパイルテスト結果は以下の通りです。
ショック!最適化すべきではない x=1 の Yul コードが失われています。次に何が起こったのか知りたい場合は、以下をお読みください。
Solidiry コンパイラ コードの UnusedStoreEliminator では、SSA 変数追跡と制御フロー追跡を使用して、ストレージ書き込み操作が冗長かどうかを判断します。カスタム関数を入力するときに、UnusedStoreEliminator で次のことが発生した場合:
メモリまたはストレージの書き込み操作: メモリおよびストレージの書き込み操作を m_store 変数に保存し、操作の初期状態を Undecded に設定します。
関数呼び出し: 関数のメモリまたはストレージの読み取りおよび書き込み操作の場所を取得し、それを m_store 変数に格納されている Undecded 状態のすべての操作と比較します。
1. m_store のストレージ操作の上書き書き込みの場合、m_store の対応する操作ステータスを未使用に変更します。
2. m_store のストレージ操作の読み取りの場合、対応する m_store の対応する操作ステータスを Used に変更します。
3. 関数にメッセージ呼び出しの実行を継続できる分岐がない場合は、m_store 内のすべてのメモリ書き込み操作を未使用に変更します。
1. アピール条件の下で、関数が実行フローを終了できる場合は、m_store でステータスが Undecded であるストレージ書き込み操作を Used に変更し、それ以外の場合は未使用としてマークします。
機能の終了: 未使用としてマークされたすべての書き込み操作を削除します。
メモリまたはストレージに書き込むための初期化コードは次のとおりです。
発生したメモリおよびストレージの書き込み操作が m_store に保存されていることがわかります。
関数呼び出しが発生したときの処理ロジック コードは次のとおりです。
このうち、operationFromFunctionCall()とapplyOperation()は、アピールの2.1と2.2の処理ロジックを実装します。以下の関数 canContinue と canTerminate に基づいて判定される If ステートメントは、2.3 ロジックを実装します。
抜け穴の存在につながるのは、以下の If 判定の欠陥であることに注意してください。 ! !
OperationFromFunctionCall() を使用して、この関数のすべてのメモリまたはストレージの読み取りおよび書き込み操作を取得します。ここで、Yul には sstore()、return() などの多くの組み込み関数があることに注意してください。ここでは、組み込み関数とユーザー定義関数に異なる処理ロジックがあることがわかります。
applyOperation() 関数は、operationFromFuncitonCall() から取得したすべての読み取り操作と書き込み操作を比較して、m_store に格納されているデータがこの関数呼び出しで読み取りまたは書き込みされるかを判断し、m_store 内の対応する操作ステータスを変更します。
Eocene コントラクトの Attack() 関数に対する前述の UnusedStoreEliminator 最適化ロジックの処理を考えてみましょう。
x= 1 を m_store 変数に格納し、ステータスを Undecded に設定します。
1. y() 関数呼び出しに遭遇し、y() 関数呼び出しのすべての読み取りおよび書き込み操作を取得します。
2. m_store 変数を調べて、y() 呼び出しによって引き起こされるすべての読み取りおよび書き込み操作が x= 1 とは何の関係もなく、x= 1 の状態がまだ未定であることを確認します。
1. y()関数の制御フローロジックを取得します。y()関数は正常に戻る分岐があるためcanContinueがTrueとなりIf判定に入りません。 x= 1 のステータスはまだ未定です。 ! !
3. x= 2 のストレージ操作が発生しました。
1. m_store 変数を調べて、x= 1 が Undecded 状態であることを確認します。操作 x= 2 は x= 1 をオーバーライドし、x= 1 の状態を Unused に設定します。
2. x= 2 の演算を m_store に格納します。初期状態は未定です。
4. 機能の終了:
1. 全m_storeの動作状態が未定状態をusedに変更します。
2. m_store の未使用状態のオペレーションをすべて削除します。
明らかに、関数が呼び出されたときに、呼び出された関数がメッセージの実行を終了できる場合、呼び出された関数の前の未決定状態のすべての書き込み操作は、未決定のままにするのではなく、使用済みに変更される必要があります。その結果、書き込み操作が実行される前に実行されます。呼び出された関数 Action が誤って削除されました。
存在する
存在するSolidity、基本的に同じロジックの下では影響を受けない契約コードの例です。ただし、コードがこの脆弱性の影響を受けないのは、UnusedStoreEliminator の処理ロジックに他の可能性があるためではなく、UnusedStoreEliminator の前の Yul 最適化ステップに、呼び出された関数を小さいか 1 つだけ埋め込む FullInliner 最適化プロセスがあるためです。 call into the call 関数では、脆弱性トリガー条件内のユーザー定義関数が回避されます。
contract Normal {
uint public x;
function f(bool a) public {
x = 1;
g(a);
x = 2;
}
function g(bool a) internal {
if (!a)
assembly { return( 0, 0) }
}
}
コンパイル結果は次のようになります。
最初のレベルのタイトル
2. 解決策
最も基本的な解決策は、影響を受ける範囲で Solidity コンパイラーを使用せずにコンパイルすることです。脆弱なバージョンのコンパイラーを使用する必要がある場合は、コンパイル中に UnusedStoreEliminator 最適化ステップを削除することを検討できます。
複数の最適化手順の複雑さと実際の関数呼び出しフローの複雑さを考慮して、コントラクト コード レベルから脆弱性を軽減したい場合は、プロのセキュリティ担当者を見つけてコード監査を実施し、これによって引き起こされるコントラクト内の脆弱性を発見してください。脆弱性に関する秘密の質問。
