スマートコントラクトの分野では、「イーサリアム仮想マシンEVM」とそのアルゴリズムとデータ構造が第一原則です。この記事では、契約を分類する必要がある理由から始まり、各シナリオが直面する可能性のある悪意のある攻撃の種類を組み合わせて、最後に比較的安全な一連の契約分類分析アルゴリズムを示します。**技術的な内容が高いですが、雑談の読み物としても使えます **分散システム間のゲームの暗い森を眺める。## 1. なぜ契約を機密扱いにする必要があるのでしょうか?とても重要なので、取引所、ウォレット、ブロックチェーンブラウザ、データ分析プラットフォームなどのDappsの根幹とも言えます!トランザクションが ERC20 転送である理由は、トランザクションの動作が少なくとも次の点で ERC20 標準に準拠しているためです。1. トランザクションのステータスは成功です2. ToアドレスはERC20規格に準拠した契約です3. Transfer 関数が呼び出され、トランザクションの CallData の最初の 4 桁が 0xa9059cbb であることが特徴です4. 実行後、To アドレスに転送イベントが送信されます。**分類を誤ると、取引行動の判断を誤ることになります**トランザクションの動作を基軸として、To アドレスを正確に分類できるかどうかで、CallData の判断はまったく異なる結論につながります。 Dapp の場合、チェーン内外の情報通信はトランザクション イベントの監視に大きく依存しており、同じイベント コードが信頼できるのは、基準を満たすコントラクトで送信された場合のみです。**分類が間違っていると、トランザクションは誤ってブラックホールに入ります**ユーザーが特定のコントラクトにトークンを移管する場合、そのコントラクトにトークン移管の関数メソッドがあらかじめ設定されていない場合、バーンと同様に資金がロックされ、資金を制御できなくなります。そして現在、多くのプロジェクトが組み込みウォレットのサポートを追加し始めているため、ユーザーのウォレットを管理することは避けられず、デプロイされた最新のコントラクトをチェーンから常にリアルタイムで分類する必要があります。資産基準。##2 分類のリスクは何でしょうか?**チェーンは身元も法則も存在しない場所であり、たとえ悪意のある取引であっても通常の取引を止めることはできません。 **彼はおばあちゃんのふりをするオオカミになることができ、おばあちゃんに期待される行動のほとんどを実行しますが、目的は家に侵入して強盗することです。**標準を主張していますが、実際には満たしていない可能性があります**一般的な分類方法は、EIP-165 標準を直接採用して、アドレスが ERC20 をサポートしているかどうかなどを読み取ることです。 もちろん、これは効率的な方法ですが、結局のところ、契約は相手方によって管理されるため、ステートメントはやっぱり鍛造。165 標準クエリは、チェーン上の限られたオペレーション コードの中で最も低いコストで資金がブラック ホールに転送されるのを防ぐ方法にすぎません。これが、以前に NFT を分析したときに、標準に SafeTransferFrom メソッドが存在すると特に言及した理由です。ここで、Safe とは、相手が NFT を転送する能力を持っているかどうかを判断するために 165 標準を使用することを指します。コントラクトのバイトコードから開始し、ソース コード レベルで静的分析を実行し、コントラクトの予想される動作から開始することによってのみ、精度を高めることができます。## 3. 契約分類スキームの設計次に、計画全体を体系的に分析します。最終的な目標は、「精度」と「効率」という 2 つの中心的な指標であることに注意してください。 **たとえ方向が正しくても、海の向こう側に到達する方法は明確ではないことを知っておく必要があります。バイトコード解析を行うには、まずコードを取得する必要があります。### 3.1. コードを取得するにはどうすればよいですか?チェーンに行くという観点から見ると、チェーン上で指定されたアドレスからバイトコードを取得できる RPC メソッドである getCode があり、 codeHash がアカウント構造に配置されているため、読み取りの点で非常に高速ですEVM の一番上にあります。しかし、この方法では特定のアドレスを取得するだけなので、さらに精度と効率を向上させたいと考えていませんか?コントラクト デプロイメント トランザクションの場合、デプロイされたコードが実行された直後、またはまだメモリ プール内にある場合でも、デプロイされたコードを取得するにはどうすればよいでしょうか?トランザクションがコントラクト ファクトリ モードの場合、トランザクションの Calldata にソース コードはありますか?結局、私のやり方はふるいのようなモードで分類することです1. コントラクトに展開されていないトランザクションの場合は、getCode を直接使用して、分類に関連するアドレスを取得します。2. 最新のメモリ プール トランザクションの場合、to アドレスが空で、CallData がコンストラクターのソース コードであるトランザクションをフィルターで除外します。3. コントラクト ファクトリ モードのトランザクションの場合、コントラクトによってデプロイされたコントラクトは、デプロイメントを実行するために他のコントラクトを呼び出すためにリサイクルされる可能性があるため、トランザクションのサブトランザクションを再帰的に分析し、タイプが CREATE である各呼び出しを記録します。または CREATE2 。デモ実装を作成したときに、rpc のバージョンが現在比較的高いことがわかりました。プロセス全体の中で最も難しい部分は、3 の実行時に指定されたタイプの呼び出しを再帰的に見つける方法であるためです。最下位レベルのメソッドは復元です。オペコードによるコンテキストの取得には驚きました。幸いなことに、現在の geth バージョンには debug\_traceTransaction メソッドがあり、オペコード操作コードを通じて各呼び出しのコンテキスト情報を整理し、コア フィールドを整理するのに役立ちます。最終的に、さまざまなデプロイメント モード (直接デプロイメント、ファクトリ モードでの単一デプロイメント、ファクトリ モードでのバッチ デプロイメント) の元のバイトコードを取得できます。### 3.2 コードから分類するには?最も簡単ですが安全ではない方法は、コードで直接文字列マッチングを行うことです。ERC20 を例にとると、標準を満たす関数は次のようになります。関数名の後には、関数の関数シグネチャが続きます。前の分析で述べたように、トランザクションは、ターゲット関数を見つけるために callData の最初の 4 桁の一致に依存します。したがって、これら 6 つの関数の署名はコントラクト バイトコードに保存する必要があります。もちろん、この方法は非常に高速で、6 つすべてを見つけることができますが、危険な要因は、Solidity コントラクトを使用して、ストレージ値 0x18160ddd の変数を設計した場合、彼は私がこの関数を持っていると考えることです。### 3.3. 正解率の向上 1- 逆コンパイルさらに正確な方法は、オペコードを逆コンパイルすることです。逆コンパイルは、取得したバイトコードをオペコードに変換するプロセスであり、より高度な逆コンパイルは、人間が読みやすい擬似コードに変換することです。今回は必要ありません。逆コンパイル方法は、付録に記載されています。記事の終わり。Solidity (高級言語) -> bytecode (バイトコード) -> opcode (オペレーションコード)明らかに特徴が見つかり、関数シグネチャは PUSH4 オペコードによって実行されるため、さらなる方法はフルテキストから PUSH4 以降の内容を抽出し、それを関数標準と照合することです。簡単なパフォーマンス実験も行いましたが、Go 言語は非常に効率的であり、10,000 回の逆コンパイルにかかる時間はわずか 220 ミリ秒であると言わざるを得ません。この後のことは難しいだろう### 3.4. 正解率向上 2-find コードブロック上記の精度率は改善されましたが、十分ではありません。全文検索 PUSH4 であるため、PUSH4 コマンドをトリガーする byte4 型の変数を構築できるためです。悩んでいたとき、オープンソースプロジェクトの実装を思いつきました ETLはチェーン上のデータを読み込んで分析するツールです ERC20と721の転送を別々のテーブルに分析するので、分類する機能が必要です契約。分析後、コード ブロックの分類に基づいており、最初の基本ブロックのみを処理していることがわかります。 [0] のpush4命令**問題は、コード ブロックを正確に判断する方法です**コード ブロックの概念は、REVERT + JUMPDEST の 2 つの連続するオペコードから来ています。関数セレクター全体のオペコード間隔で、関数が多すぎるとページめくりのロジックが表示されるため、ここには 2 つの連続するオペコードが存在する必要があります。次に、JUMPDEST コマンドも表示されます。### 3.5. 正解率向上 3-Find 関数セレクター関数セレクターの機能は、トランザクションの Calldata の最初の 4 バイトを読み取り、コード内に事前に設定されているコントラクト関数の署名と照合し、関数メソッドで指定されたメモリ位置に命令がジャンプするのを支援することです。最小限のモック実行を試してみましょうこの部分は 2 つの関数のセレクター store(uint 256) とretrieve() であり、署名は 2e64cec1, 6057361d として計算できます。逆コンパイルすると、次のオペコード文字列が得られます。これは 2 つの部分に分かれていると言えます。**最初の部分:**コンパイラでは、コントラクトの関数セレクター部分のみが callData の内容を取得します。これは、次の図に示すように、CallData の関数呼び出し署名を取得することを意味します。EVMのメモリプールの変更をシミュレートすることで効果を確認できます**第二部:**セレクターの値と一致するかどうかを判定する処理1.retrieve() の 4 バイトの関数シグネチャ (0x2e64cec1) をスタックに渡します。2. EQ オペコードはスタック領域から 2 つの変数、つまり 0x2e64cec1 と 0x6057361d をポップし、それらが等しいかどうかをチェックします。3. PUSH2 は 2 バイトのデータ (ここでは 0x003b、10 進数で 59) をスタックに転送します スタック領域にはプログラム カウンタがあり、バイトコード内の次の実行コマンドの位置を指定します。ここでは 59 を設定します。これは、retrieve() バイトコードが始まる場所だからです。4. JUMPI は「Jump to if...」の略で、入力としてスタックから 2 つの値をポップし、条件が true の場合、プログラム カウンタは 59 に更新されます。これは、EVM がコントラクト内の関数呼び出しに基づいて、実行する必要がある関数バイトコードの場所を決定する方法です。実際には、これはコントラクト内のすべての関数とそのジャンプ先に対する単純な「if ステートメント」のセットにすぎません。## 4. スキームの概要全体的な概要は次のとおりです1. 各コントラクト アドレスは、GO の VM および ASM ライブラリを使用して、rpcgetcode または debug\_traceTransaction を通じてデプロイメント後にバイトコードを取得し、逆コンパイル後にオペコードを取得できます。2. EVM 運用の原則において、契約は以下の特徴を持ちます。+ コードブロックの区別として REVERT+JUMPDEST を使用する+ コントラクトには関数セレクターの機能が必要であり、この関数は最初のコード ブロックにもなければなりません+ 関数セレクターでは、その関数メソッドはすべてオペコードとして PUSH4 を使用します。+ このセレクターに含まれるオペコードには、連続する PUSH1 00; CALLDATALOAD; PUSH1 e0; SHR; DUP1 があります。コア機能は callDate データをロードし、ディスプレイスメント操作を実行することです。コントラクト関数からは、他の構文は生成されません3. 対応する関数シグネチャは eip で定義されており、必須およびオプションのクリア命令があります。### 4.1. 一意性の証明現時点で、高効率かつ高精度な契約分析手法が基本的に実現されていると言えますが、もちろん、これまでずっと厳密にやってきたのですから、さらに厳密化してもいいのではないかと思います。 REVER+JUMPDEST を使用してコード ブロックを区別し、避けられない CallDate のロードとディスプレイスメントを組み合わせて独自の判断を行う ソリッドティ コントラクトを使用して同様のオペコード シーケンスを実装できることは存在しますか?制御実験をしてみましたが、ソリディティ文法レベルからmsg.sigなどのCallDataを取得する方法はありますが、コンパイル後のオペコードの実装方法が異なります。
徹底したEVM - 契約分類という些細な事柄の背後にあるリスク
スマートコントラクトの分野では、「イーサリアム仮想マシンEVM」とそのアルゴリズムとデータ構造が第一原則です。
この記事では、契約を分類する必要がある理由から始まり、各シナリオが直面する可能性のある悪意のある攻撃の種類を組み合わせて、最後に比較的安全な一連の契約分類分析アルゴリズムを示します。
**技術的な内容が高いですが、雑談の読み物としても使えます **分散システム間のゲームの暗い森を眺める。
1. なぜ契約を機密扱いにする必要があるのでしょうか?
とても重要なので、取引所、ウォレット、ブロックチェーンブラウザ、データ分析プラットフォームなどのDappsの根幹とも言えます!
トランザクションが ERC20 転送である理由は、トランザクションの動作が少なくとも次の点で ERC20 標準に準拠しているためです。
分類を誤ると、取引行動の判断を誤ることになります
トランザクションの動作を基軸として、To アドレスを正確に分類できるかどうかで、CallData の判断はまったく異なる結論につながります。 Dapp の場合、チェーン内外の情報通信はトランザクション イベントの監視に大きく依存しており、同じイベント コードが信頼できるのは、基準を満たすコントラクトで送信された場合のみです。
分類が間違っていると、トランザクションは誤ってブラックホールに入ります
ユーザーが特定のコントラクトにトークンを移管する場合、そのコントラクトにトークン移管の関数メソッドがあらかじめ設定されていない場合、バーンと同様に資金がロックされ、資金を制御できなくなります。
そして現在、多くのプロジェクトが組み込みウォレットのサポートを追加し始めているため、ユーザーのウォレットを管理することは避けられず、デプロイされた最新のコントラクトをチェーンから常にリアルタイムで分類する必要があります。資産基準。
##2 分類のリスクは何でしょうか?
**チェーンは身元も法則も存在しない場所であり、たとえ悪意のある取引であっても通常の取引を止めることはできません。 **
彼はおばあちゃんのふりをするオオカミになることができ、おばあちゃんに期待される行動のほとんどを実行しますが、目的は家に侵入して強盗することです。
標準を主張していますが、実際には満たしていない可能性があります
一般的な分類方法は、EIP-165 標準を直接採用して、アドレスが ERC20 をサポートしているかどうかなどを読み取ることです。 もちろん、これは効率的な方法ですが、結局のところ、契約は相手方によって管理されるため、ステートメントはやっぱり鍛造。
165 標準クエリは、チェーン上の限られたオペレーション コードの中で最も低いコストで資金がブラック ホールに転送されるのを防ぐ方法にすぎません。
これが、以前に NFT を分析したときに、標準に SafeTransferFrom メソッドが存在すると特に言及した理由です。ここで、Safe とは、相手が NFT を転送する能力を持っているかどうかを判断するために 165 標準を使用することを指します。
コントラクトのバイトコードから開始し、ソース コード レベルで静的分析を実行し、コントラクトの予想される動作から開始することによってのみ、精度を高めることができます。
3. 契約分類スキームの設計
次に、計画全体を体系的に分析します。最終的な目標は、「精度」と「効率」という 2 つの中心的な指標であることに注意してください。 **
たとえ方向が正しくても、海の向こう側に到達する方法は明確ではないことを知っておく必要があります。バイトコード解析を行うには、まずコードを取得する必要があります。
3.1. コードを取得するにはどうすればよいですか?
チェーンに行くという観点から見ると、チェーン上で指定されたアドレスからバイトコードを取得できる RPC メソッドである getCode があり、 codeHash がアカウント構造に配置されているため、読み取りの点で非常に高速ですEVM の一番上にあります。
しかし、この方法では特定のアドレスを取得するだけなので、さらに精度と効率を向上させたいと考えていませんか?
コントラクト デプロイメント トランザクションの場合、デプロイされたコードが実行された直後、またはまだメモリ プール内にある場合でも、デプロイされたコードを取得するにはどうすればよいでしょうか?
トランザクションがコントラクト ファクトリ モードの場合、トランザクションの Calldata にソース コードはありますか?
結局、私のやり方はふるいのようなモードで分類することです
デモ実装を作成したときに、rpc のバージョンが現在比較的高いことがわかりました。プロセス全体の中で最も難しい部分は、3 の実行時に指定されたタイプの呼び出しを再帰的に見つける方法であるためです。最下位レベルのメソッドは復元です。オペコードによるコンテキストの取得には驚きました。
幸いなことに、現在の geth バージョンには debug_traceTransaction メソッドがあり、オペコード操作コードを通じて各呼び出しのコンテキスト情報を整理し、コア フィールドを整理するのに役立ちます。
最終的に、さまざまなデプロイメント モード (直接デプロイメント、ファクトリ モードでの単一デプロイメント、ファクトリ モードでのバッチ デプロイメント) の元のバイトコードを取得できます。
3.2 コードから分類するには?
最も簡単ですが安全ではない方法は、コードで直接文字列マッチングを行うことです。ERC20 を例にとると、標準を満たす関数は次のようになります。
関数名の後には、関数の関数シグネチャが続きます。前の分析で述べたように、トランザクションは、ターゲット関数を見つけるために callData の最初の 4 桁の一致に依存します。
したがって、これら 6 つの関数の署名はコントラクト バイトコードに保存する必要があります。
もちろん、この方法は非常に高速で、6 つすべてを見つけることができますが、危険な要因は、Solidity コントラクトを使用して、ストレージ値 0x18160ddd の変数を設計した場合、彼は私がこの関数を持っていると考えることです。
3.3. 正解率の向上 1- 逆コンパイル
さらに正確な方法は、オペコードを逆コンパイルすることです。逆コンパイルは、取得したバイトコードをオペコードに変換するプロセスであり、より高度な逆コンパイルは、人間が読みやすい擬似コードに変換することです。今回は必要ありません。逆コンパイル方法は、付録に記載されています。記事の終わり。
Solidity (高級言語) -> bytecode (バイトコード) -> opcode (オペレーションコード)
明らかに特徴が見つかり、関数シグネチャは PUSH4 オペコードによって実行されるため、さらなる方法はフルテキストから PUSH4 以降の内容を抽出し、それを関数標準と照合することです。
簡単なパフォーマンス実験も行いましたが、Go 言語は非常に効率的であり、10,000 回の逆コンパイルにかかる時間はわずか 220 ミリ秒であると言わざるを得ません。
この後のことは難しいだろう
3.4. 正解率向上 2-find コードブロック
上記の精度率は改善されましたが、十分ではありません。全文検索 PUSH4 であるため、PUSH4 コマンドをトリガーする byte4 型の変数を構築できるためです。
悩んでいたとき、オープンソースプロジェクトの実装を思いつきました ETLはチェーン上のデータを読み込んで分析するツールです ERC20と721の転送を別々のテーブルに分析するので、分類する機能が必要です契約。
分析後、コード ブロックの分類に基づいており、最初の基本ブロックのみを処理していることがわかります。 [0] のpush4命令
問題は、コード ブロックを正確に判断する方法です
コード ブロックの概念は、REVERT + JUMPDEST の 2 つの連続するオペコードから来ています。関数セレクター全体のオペコード間隔で、関数が多すぎるとページめくりのロジックが表示されるため、ここには 2 つの連続するオペコードが存在する必要があります。次に、JUMPDEST コマンドも表示されます。
3.5. 正解率向上 3-Find 関数セレクター
関数セレクターの機能は、トランザクションの Calldata の最初の 4 バイトを読み取り、コード内に事前に設定されているコントラクト関数の署名と照合し、関数メソッドで指定されたメモリ位置に命令がジャンプするのを支援することです。
最小限のモック実行を試してみましょう
この部分は 2 つの関数のセレクター store(uint 256) とretrieve() であり、署名は 2e64cec1, 6057361d として計算できます。
逆コンパイルすると、次のオペコード文字列が得られます。これは 2 つの部分に分かれていると言えます。
最初の部分:
コンパイラでは、コントラクトの関数セレクター部分のみが callData の内容を取得します。これは、次の図に示すように、CallData の関数呼び出し署名を取得することを意味します。
EVMのメモリプールの変更をシミュレートすることで効果を確認できます
第二部:
セレクターの値と一致するかどうかを判定する処理
1.retrieve() の 4 バイトの関数シグネチャ (0x2e64cec1) をスタックに渡します。
EQ オペコードはスタック領域から 2 つの変数、つまり 0x2e64cec1 と 0x6057361d をポップし、それらが等しいかどうかをチェックします。
PUSH2 は 2 バイトのデータ (ここでは 0x003b、10 進数で 59) をスタックに転送します スタック領域にはプログラム カウンタがあり、バイトコード内の次の実行コマンドの位置を指定します。ここでは 59 を設定します。これは、retrieve() バイトコードが始まる場所だからです。
JUMPI は「Jump to if...」の略で、入力としてスタックから 2 つの値をポップし、条件が true の場合、プログラム カウンタは 59 に更新されます。
これは、EVM がコントラクト内の関数呼び出しに基づいて、実行する必要がある関数バイトコードの場所を決定する方法です。
実際には、これはコントラクト内のすべての関数とそのジャンプ先に対する単純な「if ステートメント」のセットにすぎません。
4. スキームの概要
全体的な概要は次のとおりです
4.1. 一意性の証明
現時点で、高効率かつ高精度な契約分析手法が基本的に実現されていると言えますが、もちろん、これまでずっと厳密にやってきたのですから、さらに厳密化してもいいのではないかと思います。 REVER+JUMPDEST を使用してコード ブロックを区別し、避けられない CallDate のロードとディスプレイスメントを組み合わせて独自の判断を行う ソリッドティ コントラクトを使用して同様のオペコード シーケンスを実装できることは存在しますか?
制御実験をしてみましたが、ソリディティ文法レベルからmsg.sigなどのCallDataを取得する方法はありますが、コンパイル後のオペコードの実装方法が異なります。