【入門編】 デリゲートコール(delegatecall)の危険性とストレージ衝突 – IoT・OT(制御システム) & ブロックチェーンセキュリティ防御ガイド

ブロックチェーンの「間取り」を間違えると家ごと乗っ取られる? delegatecall の恐ろしい罠

こんにちは!セキュリティの世界へようこそ。今日は、ブロックチェーン開発において「最もやってはいけないミス」の一つ、「delegatecall(デリゲートコール)によるストレージ衝突」について解説します。

「ストレージ衝突? 何だか難しそう……」と思ったあなたも大丈夫。身近な例え話から、少しずつ紐解いていきましょう。

—

1. 例え話で理解する:あなたの家の「間取り図」が書き換わる恐怖

想像してみてください。あなたは、最新のスマート家電を管理する「コントローラー」という名の家を建てたとします。この家には、以下の3つの引き出しがあります。

1. 引き出しA:家の鍵
2. 引き出しB:金庫の暗証番号
3. 引き出しC:庭の植木の名前

さて、あなたは家をアップグレードするために、「外部の便利なツール(ライブラリ)」を借りてくることにしました。ここで使うのが delegatecall です。

delegatecall とは、「自分の家の引き出しを、他人のツールに直接操作させる」という仕組みです。

ここで大問題が発生します。もし、そのツールが「引き出しAには金庫の暗証番号を入れるべきだ」という全く別の間取り図を持っていたらどうなるでしょう?

あなたが「引き出しAの内容を更新して!」と頼んだ瞬間、ツールは「よし、金庫の暗証番号を更新するぞ!」と勘違いして、大事な情報を上書きしてしまうのです。これが「ストレージ衝突」の正体です。

—

2. なぜこれが「脆弱性」になるのか?

ブロックチェーン(イーサリアムなど)において、コントラクトのデータは「スロット」と呼ばれる箱に順番に格納されています。

  • スロット0: owner (誰が管理者か)
  • スロット1: balance (いくら持っているか)

delegatecall を使うと、呼び出し先のコードが「自分の家のスロット」を勝手に書き換えます。もし呼び出し先のストレージ構造が少しでもズレていたら、本来書き換えるはずのない owner の値が、攻撃者のアドレスに書き換わってしまうというわけです。

—

3. 実践!脆弱なコードを見てみよう

まずは、「やってはいけない例」を見てみましょう。

// 脆弱なコントラクト(家主)
contract VulnerableWallet {
    address public owner; // スロット0
    uint256 public balance; // スロット1

    function updateBalance(address logic, uint256 newBalance) public {
        // 危険な呼び出し:外部のロジックにストレージを操作させる
        logic.delegatecall(abi.encodeWithSignature("setBalance(uint256)", newBalance));
    }
}

// 攻撃者が用意した不正なライブラリ
contract MaliciousLibrary {
    address public attacker; // スロット0(ここでオーナーが書き換わる!)
    uint256 public balance;  // スロット1

    function setBalance(uint256 _balance) public {
        // 本来はバランスを変えるはずが、自分をオーナーに昇格させる
        attacker = msg.sender; 
    }
}

このコードでは、VulnerableWallet の owner が、MaliciousLibrary の attacker と同じ「スロット0」に配置されています。これにより、バランスを変えるつもりが、家の所有権を奪われてしまうのです。

—

4. どうやって防ぐのか?一歩ずつ対策を学ぼう!

この攻撃を防ぐための「防犯対策」は大きく分けて2つあります。

① ストレージレイアウトを完全に一致させる

プロキシ(代理人)を使う場合は、呼び出し元と呼び出し先のストレージ構造を1ビットの狂いもなく一致させなければなりません。しかし、これは非常にミスが起きやすい手法です。

② ライブラリには library キーワードを使う

Solidityには、最初から delegatecall を安全に使うための library という仕組みがあります。これを使うと、ライブラリ側は自分自身のストレージを持つことができないため、そもそも「衝突」が発生しません。

// 安全なライブラリの書き方
library SafeMath {
    // 状態変数を持たないので衝突のしようがない
    function add(uint256 a, uint256 b) internal pure returns (uint256) {
        return a + b;
    }
}

—

最後に:セキュリティは「疑うこと」から始まる

今回学んだ「ストレージ衝突」は、一見すると些細なミスに見えます。しかし、ハッカーはこの「わずかな間取りのズレ」を見逃しません。

  • 外部からのコードを呼び出すときは「自分の家の引き出し」に触れさせないこと。
  • どうしても使うなら、間取り図(ストレージレイアウト)が完全に一致しているか、何度も確認すること。

これらを意識するだけで、あなたの書くスマートコントラクトは格段に強固なものになります。「便利さ」の裏には常にリスクが潜んでいる。それを忘れずに、一歩ずつ安全なコードを書いていきましょう!

もし、「自分のコードが安全か不安……」という方がいたら、まずはOpenZeppelinが提供している UUPSUpgradeable などの標準的なプロキシパターンを調べることから始めてみてください。業界のプロたちが作った「鉄壁の防犯門」をまずは活用するのが、一番の近道ですよ。

コメント

タイトルとURLをコピーしました