メインコンテンツへスキップ

スリザーを使用してスマートコントラクトのバグを見つける方法

Solidity
スマートコントラクト
セキュリティ
テスト
上級
トレイルオブビッツ
2020年6月9日
13 分で読めます

スリザーの使用方法

このチュートリアルの目的は、スリザーを使用してスマートコントラクトのバグを自動的に見つける方法を示すことです。

インストール

スリザーにはPython 3.6以上が必要です。pipまたはDockerを使用してインストールできます。

pipを使用したスリザーのインストール:

pip3 install --user slither-analyzer

Dockerを使用したスリザーのインストール:

docker pull trailofbits/eth-security-toolbox
docker run -it -v "$PWD":/home/trufflecon trailofbits/eth-security-toolbox

最後のコマンドは、現在のディレクトリにアクセスできるDocker内でeth-security-toolboxを実行します。ホストからファイルを変更し、Dockerからそのファイルに対してツールを実行できます。

Docker内で以下を実行します:

solc-select 0.5.11
cd /home/trufflecon/

スクリプトの実行

Python 3でPythonスクリプトを実行するには:

python3 script.py

コマンドライン

コマンドラインとユーザー定義スクリプトの比較。 スリザーには、多くの一般的なバグを見つけるための事前定義された検出器のセットが付属しています。コマンドラインからスリザーを呼び出すと、すべての検出器が実行されるため、静的解析に関する詳細な知識は必要ありません。

slither project_paths

検出器に加えて、スリザーにはプリンター (opens in a new tab)ツール (opens in a new tab)によるコードレビュー機能があります。

プライベート検出器やGitHub統合にアクセスするには、crytic.io (opens in a new tab)を使用してください。

静的解析

スリザーの静的解析フレームワークの機能と設計については、ブログ記事(1 (opens in a new tab)2 (opens in a new tab))および学術論文 (opens in a new tab)で説明されています。

静的解析にはさまざまな種類があります。clang (opens in a new tab)gcc (opens in a new tab)のようなコンパイラがこれらの研究技術に依存していることはご存知かもしれませんが、Infer (opens in a new tab)CodeClimate (opens in a new tab)FindBugs (opens in a new tab)、およびFrama-C (opens in a new tab)Polyspace (opens in a new tab)のような形式手法に基づくツールの基盤にもなっています。

ここでは、静的解析の技術や研究について網羅的にレビューすることはしません。代わりに、バグを見つけたりコードを理解したりするためにスリザーをより効果的に使用できるよう、スリザーの仕組みを理解するために必要なことに焦点を当てます。

コード表現

単一の実行パスについて推論する動的解析とは対照的に、静的解析はすべてのパスについて一度に推論します。そのためには、異なるコード表現に依存します。最も一般的な2つは、抽象構文木(AST)と制御フローグラフ(CFG)です。

抽象構文木(AST)

ASTは、コンパイラがコードを解析するたびに使用されます。これはおそらく、静的解析を実行できる最も基本的な構造です。

簡単に言えば、ASTは構造化されたツリーであり、通常、各葉には変数または定数が含まれ、内部ノードはオペランドまたは制御フロー操作です。次のコードを考えてみましょう。

function safeAdd(uint a, uint b) pure internal returns(uint){
    if(a + b <= a){
        revert();
    }
    return a + b;
}

対応するASTは以下のようになります。

AST

スリザーはsolcによってエクスポートされたASTを使用します。

構築は簡単ですが、ASTはネストされた構造です。そのため、解析が最も簡単ではない場合があります。たとえば、式a + b <= aで使用される操作を特定するには、まず<=を解析し、次に+を解析する必要があります。一般的なアプローチは、ツリーを再帰的にナビゲートする、いわゆるビジターパターンを使用することです。スリザーには、ExpressionVisitor (opens in a new tab)に汎用ビジターが含まれています。

次のコードは、ExpressionVisitorを使用して、式に加算が含まれているかどうかを検出します。

制御フローグラフ(CFG)

2番目に一般的なコード表現は、制御フローグラフ(CFG)です。その名の通り、すべての実行パスを公開するグラフベースの表現です。各ノードには1つまたは複数の命令が含まれます。グラフ内のエッジは、制御フロー操作(if/then/else、ループなど)を表します。前の例のCFGは次のとおりです。

CFG

CFGは、ほとんどの解析が構築される基盤となる表現です。

他にも多くのコード表現が存在します。実行したい解析に応じて、各表現には長所と短所があります。

解析

スリザーで実行できる最も単純なタイプの解析は、構文解析です。

構文解析

スリザーは、パターンマッチングのようなアプローチを使用して、コードのさまざまなコンポーネントとその表現をナビゲートし、不整合や欠陥を見つけることができます。

たとえば、次の検出器は構文関連の問題を探します。

セマンティック解析

構文解析とは対照的に、セマンティック解析はより深く掘り下げ、コードの「意味」を解析します。このファミリーには、いくつかの幅広いタイプの解析が含まれます。これらはより強力で有用な結果をもたらしますが、記述するのもより複雑になります。

セマンティック解析は、最も高度な脆弱性検出に使用されます。

データ依存性解析

変数variable_aの値がvariable_bの影響を受けるパスが存在する場合、変数variable_avariable_bにデータ依存していると言われます。

次のコードでは、variable_avariable_bに依存しています。

// ...
variable_a = variable_b + 1;

スリザーには、中間表現(後のセクションで説明します)のおかげで、組み込みのデータ依存性 (opens in a new tab)機能が備わっています。

データ依存性の使用例は、危険な厳密等価検出器 (opens in a new tab)で見ることができます。ここでスリザーは、危険な値との厳密な等価比較(incorrect_strict_equality.py#L86-L87 (opens in a new tab))を探し、攻撃者がコントラクトを罠にかけるのを防ぐために、==ではなく>=または<=を使用する必要があることをユーザーに通知します。とりわけ、この検出器はbalanceOf(address)への呼び出しの戻り値(incorrect_strict_equality.py#L63-L64 (opens in a new tab))を危険と見なし、データ依存性エンジンを使用してその使用状況を追跡します。

不動点計算

解析がCFGをナビゲートし、エッジをたどる場合、すでに訪問したノードに遭遇する可能性があります。たとえば、以下に示すようなループが存在する場合です。

for(uint i; i < range; ++){
    variable_a += 1
}

解析はいつ停止すべきかを知る必要があります。ここには2つの主な戦略があります。(1)各ノードを有限回反復する、(2)いわゆる_不動点(fixpoint)_を計算する。不動点とは基本的に、このノードを解析しても意味のある情報が得られないことを意味します。

使用される不動点の例は、リエントランシー検出器で見ることができます。スリザーはノードを探索し、外部呼び出し、ストレージへの書き込みと読み取りを探します。不動点(reentrancy.py#L125-L131 (opens in a new tab))に達すると探索を停止し、さまざまなリエントランシーパターン(reentrancy_benign.py (opens in a new tab)reentrancy_read_before_write.py (opens in a new tab)reentrancy_eth.py (opens in a new tab))を通じて結果を解析し、リエントランシーが存在するかどうかを確認します。

効率的な不動点計算を使用して解析を記述するには、解析がどのように情報を伝播するかをよく理解する必要があります。

中間表現

中間表現(IR)は、元の言語よりも静的解析に適した言語です。スリザーはSolidityを独自のIRであるSlithIR (opens in a new tab)に変換します。

基本的なチェックのみを記述したい場合は、SlithIRを理解する必要はありません。ただし、高度なセマンティック解析を記述する予定がある場合は役立ちます。SlithIR (opens in a new tab)およびSSA (opens in a new tab)プリンターは、コードがどのように変換されるかを理解するのに役立ちます。

APIの基本

スリザーには、コントラクトとその関数の基本的な属性を探索できるAPIがあります。

コードベースをロードするには:

from slither import Slither
slither = Slither('/path/to/project')

コントラクトと関数の探索

Slitherオブジェクトには以下が含まれます:

  • contracts (list(Contract): コントラクトのリスト
  • contracts_derived (list(Contract): 他のコントラクトに継承されていないコントラクトのリスト(コントラクトのサブセット)
  • get_contract_from_name (str): 名前からコントラクトを返す

Contractオブジェクトには以下が含まれます:

  • name (str): コントラクトの名前
  • functions (list(Function)): 関数のリスト
  • modifiers (list(Modifier)): 関数のリスト
  • all_functions_called (list(Function/Modifier)): コントラクトから到達可能なすべての内部関数のリスト
  • inheritance (list(Contract)): 継承されたコントラクトのリスト
  • get_function_from_signature (str): 署名から関数を返す
  • get_modifier_from_signature (str): 署名から修飾子を返す
  • get_state_variable_from_name (str): 名前から状態変数を返す

FunctionまたはModifierオブジェクトには以下が含まれます:

  • name (str): 関数の名前
  • contract (contract): 関数が宣言されているコントラクト
  • nodes (list(Node)): 関数/修飾子のCFGを構成するノードのリスト
  • entry_point (Node): CFGのエントリポイント
  • variables_read (list(Variable)): 読み取られた変数のリスト
  • variables_written (list(Variable)): 書き込まれた変数のリスト
  • state_variables_read (list(StateVariable)): 読み取られた状態変数のリスト(variables`readのサブセット)
  • state_variables_written (list(StateVariable)): 書き込まれた状態変数のリスト(variables`writtenのサブセット)