Reentrancy攻撃について深く理解し、効果的な防止方法を学ぶ

**Reentrancy攻撃とは**

Reentrancy攻撃は、スマートコントラクトの脆弱性を利用する攻撃方法です。攻撃者は外部関数を呼び出した後、状態を更新する前に同じ関数を再度呼び出すことで、契約から不正な価値を引き出します。

**攻撃メカニズム**

1. 攻撃者が対象契約の関数を呼び出す
2. 対象契約が攻撃者のコントラクトに資金を送信する
3. 攻撃者のコントラクトが再度対象契約の関数を呼び出す
4. このプロセスが状態チェックなしで繰り返される

**防止方法**

**1. チェック・エフェクト・インタラクション(CEI)パターン**
- 状態変数を最初にチェック
- 状態を更新
- 外部呼び出しを実行

**2. Mutex ロック**
```
modifier noReentrancy {
require(!locked);
locked = true;
_;
locked = false;
}
```

**3. OpenZeppelinの ReentrancyGuard を使用**
```
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
```

**4. 低レベル呼び出しの回避**
- `.transfer()` または `.send()` を使用
- `.call()` の前に状態を更新

**実装例**

安全な引き出し関数:
```
function withdraw(uint amount) public nonReentrant {
require(balances[msg.sender] >= amount);
balances[msg.sender] -= amount;
(bool success,) = msg.sender.call{value: amount}("");
require(success);
}
```

**監査とテスト**

- 形式検証ツールを使用
- セキュリティ監査会社に依頼
- テストネットで徹底的にテスト
- イベントログで監視

Reentrancy攻撃から保護することは、DeFiアプリケーション開発における必須事項です。

再入攻撃はスマートコントラクトにとって最も深刻なセキュリティ脅威の一つです。この記事では、再入攻撃の仕組みだけでなく、プロジェクトを包括的に守る方法も解説します。

まず基本的な概念から始め、実際の攻撃コードを分析し、最後に証明された3つの防御技術を紹介します。nonReentrant()修飾子からGlobalReentrancyGuard()、そして「チェック・効果・相互作用」パターンまでです。

再入攻撃とは何ですか?

二つのスマートコントラクトが相互に作用している様子を想像してください。ContractAとContractBは互いに呼び出し合うことが可能です。これは普通のことに見えますが、これが攻撃者にとっての脆弱点となり得ます。

再入攻撃の核心は:あるスマートコントラクトが、他のコントラクトを呼び出している最中に、そのコントラクトを再度呼び出すことができる点にあります。これにより、制御されていない無限ループが発生する可能性があります。

例を挙げると、ContractAが10 Etherを保持しており、その中にContractBが1 Etherを送金している場合を考えます。通常、ContractBが引き出しを要求するときは、残高(>0)を確認し、Etherを受け取り、残高を0に設定します。しかし、再入攻撃では、残高の更新前に何度もこの過程を繰り返すことが可能です。

再入攻撃の仕組み

攻撃者には二つの主要な関数が必要です:attack()とfallback()です。

fallback()はSolidityの特殊な関数で、名前や引数、戻り値を持たない外部関数です。次の場合に自動的に呼び出されます:

  • 存在しない関数を呼び出されたとき
  • 関数指定なしでデータが送信されたとき
  • Etherだけが送られたとき

攻撃の流れは次の通りです。

ステップ1: 攻撃者がattack()を呼び出し、その中でContractAからの引き出しを開始します。

ステップ2: ContractAは、ContractBの残高が>0かどうかを確認し、1 Etherを送信してfallback()を呼び出します。

ステップ3: ここが重要なポイントです。残高の更新はまだ行われていません。ContractAは引き続き引き出し処理を実行中です。

ステップ4: fallback()が呼び出され、即座にContractAの引き出し関数を再度呼び出します。

ステップ5: ContractAは再び残高を確認し、まだ残高があるため、もう一度Etherを送信しfallback()を呼び出します。

このループは、ContractAのEtherが尽きるまで続きます。これが再入攻撃の危険性です。

攻撃コードの具体例

EtherStoreというコントラクトを例にとると、二つの主要な関数があります:

  • deposit():送金者の残高を記録・更新
  • withdrawAll():全残高を一度に引き出す

withdrawAll()は次の順序で動作します:残高の確認(>0)、Etherの送信、残高の0設定。

この順序に脆弱性があり、Etherの送信が残高の更新前に行われるため、攻撃者はこれを悪用できます。

攻撃用コントラクトは次のように設計されています:

  • EtherStoreのアドレスを受け取るコンストラクタ
  • Etherが残っている限りwithdrawAll()を呼び続けるfallback()
  • 攻撃開始用のattack()関数

攻撃の流れはシンプルです:

  1. attack()をEtherとともに呼び出す
  2. attack()がEtherをEtherStoreに送る
  3. attack()がEtherStoreのwithdrawAll()を呼び出す
  4. EtherStoreがEtherを送信し、fallback()が呼び出される
  5. fallback()が再びwithdrawAll()を呼び出す
  6. このループはEtherが尽きるまで続きます。

再入攻撃対策の3つの技術

技術1:nonReentrant修飾子の使用

最も簡単な方法は、特定の関数に対してnonReentrant修飾子を付与することです。修飾子はSolidityの特殊な関数で、他の関数に条件や追加処理を付加できます。

nonReentrantの仕組み:

  • 関数実行開始時にロックをかける
  • 再呼び出しがあれば拒否
  • 関数終了時にロックを解除

欠点は:これが一つの関数だけを保護し、複数の関数間の再入を防ぐわけではない点です。

技術2:チェック・効果・相互作用パターン

より洗練された方法は、関数の実行順序を工夫することです。

このパターンは次の3段階からなります:

  • Check(検査):必要条件を事前に検証(require)
  • Effect(効果):状態を即座に更新
  • Interaction(相互作用):外部コントラクト呼び出しやEther送信は最後に行う

例:

  • 脆弱なコード:検査 → Ether送信 → 状態更新
  • 安全なコード:検査 → 状態更新 → Ether送信

状態を先に更新しておくことで、再呼び出しがあっても状態は既に更新済みとなり、攻撃を防ぎます。

技術3:GlobalReentrancyGuardによる複数コントラクトの保護

この技術は、多数のコントラクトが相互作用する複雑なシステム向けです。中央のガードコントラクトを作り、全体の再入を管理します。

各コントラクトは個別のロック変数ではなく、共通のGlobalReentrancyGuardを参照します。これにより:

  • 異なるコントラクト間の再入も制御
  • 複雑なチェーン再入攻撃も防止

例:ScheduledTransferがEtherを送るとき、AttackTransferのfallback()が他の関数を呼び出そうとした場合も、ガードが検知して阻止します。

適切な防御技術の選び方

nonReentrantの利用:

  • 特定の関数だけを守りたい場合
  • コストやオーバーヘッドを抑えたい場合
  • 関数間の再入を気にしない場合

Check-Effect-Interactionの採用:

  • コスト最適化を重視
  • 複数の関数を保護したい
  • ベストプラクティスを全体に適用したい場合

GlobalReentrancyGuardの利用:

  • 複雑なコントラクトシステム
  • 多くのコントラクトが相互作用
  • 一元的な保護を望む場合

これらを組み合わせて、プロジェクトのニーズに合わせて最適な防御策を選びましょう。

まとめ

再入攻撃は理解と適切な対策で防ぐことが可能です。modifier nonReentrantからGlobalReentrancyGuardまで、それぞれのツールはセキュリティの層を築きます。

セキュリティは選択肢ではなく必須です。再入攻撃の仕組みを理解し、適切な防御策を実装することで、安全かつ効率的なスマートコントラクトを構築しましょう。

Web3やSolidityのセキュリティ、スマートコントラクトの監査に関する最新情報は、@TheBlockChainerをTwitterでフォローしてください。

原文表示
このページには第三者のコンテンツが含まれている場合があり、情報提供のみを目的としております(表明・保証をするものではありません)。Gateによる見解の支持や、金融・専門的な助言とみなされるべきものではありません。詳細については免責事項をご覧ください。
  • 報酬
  • コメント
  • リポスト
  • 共有
コメント
コメントを追加
コメントを追加
コメントなし
  • ピン