【実務・中級編】 コントラクトの自己破壊(selfdestruct)による資金凍結 – IoT・OT(制御システム) & ブロックチェーンセキュリティ防御ガイド

「残高で判定するな」― selfdestruct が招くスマートコントラクトの死角

現場でコードを叩いていると、たまに「なぜこんな単純なミスを?」と頭を抱えたくなる設計に出会う。今回は、スマートコントラクト、特にEVM系チェーンにおいて、多くのエンジニアが陥る「コントラクト残高への過度な信頼」が引き起こす脆弱性について話そう。

特に、selfdestruct(現在は非推奨だが、依然として過去のコントラクトには存在する)を悪用した資金凍結や、強制送金によるロジック破壊は、Web3特有の「泥臭い」攻撃ベクトルだ。

—

なぜ selfdestruct でコントラクトが死ぬのか?

多くの開発者は、address(this).balance を見て「ここには〇〇ETH入っているから、これを使って報酬を計算しよう」というロジックを書きがちだ。これが致命傷になる。

攻撃者は selfdestruct(target) を実行することで、コントラクトのロジックを通さず、問答無用で強制的にETHを送りつけることが可能だ。

例えば、「コントラクトの残高が100ETHを超えたら、ある条件で送金する」というコントラクトがあったとする。攻撃者が外部から強制的にETHを流し込めば、ロジックは即座に崩壊し、コントラクトは「正常に動くはずのロジック」を完遂できなくなる。これが、資金凍結や計算ロジックの破綻を招くメカニズムだ。

—

具体的な攻撃手法:PoCの考え方

攻撃者は以下のような「空のコントラクト」を用意し、selfdestruct を呼び出す。

// 攻撃者のコントラクト例
contract Attacker {
    address payable target;

    constructor(address payable _target) {
        target = _target;
    }

    // コンストラクタでETHを受け取り、即座にターゲットへ自爆攻撃を行う
    function attack() external payable {
        selfdestruct(target);
    }
}

このコードを実行されると、target に指定されたコントラクトの address(this).balance は、どんなに防御を固めていても強制的にインクリメントされる。もしあなたの計算式が「現在の残高」に依存していたら、その瞬間からあなたのシステムは「汚染」されたことになる。

—

どう防御すべきか?:実務的な解決策

解決策はシンプルだ。「コントラクトの残高(balance)を、ロジックの判定基準に使うな」。

その代わり、内部変数で「管理すべき残高」を追跡し、それを基準に計算を行うこと。外部からの強制送金は、この内部変数には影響を与えないため、ロジックを安全に保てる。

以下に、セキュアな実装パターンを示す。JavaScript(ethers.js等)からコントラクトを操作する際も、この設計思想を意識してほしい。

セキュアなコントラクトの実装例(Solidity)

// 推奨される実装パターン
contract SecureVault {
    // 外部の残高に頼らず、独自に追跡する変数
    uint256 public internalBalance;

    function deposit() external payable {
        // 残高を管理変数で正確にトラッキングする
        internalBalance += msg.value;
    }

    function withdraw(uint256 amount) external {
        // 判定には必ず internalBalance を使用する
        require(internalBalance >= amount, "Insufficient funds");
        
        internalBalance -= amount;
        payable(msg.sender).transfer(amount);
    }
}

—

Web3インフラ担当者への提言

コントラクトコードだけでなく、システム全体での防御も重要だ。

1. アクセス制限と監査: selfdestruct が含まれる古いライブラリの利用は厳禁だ。コンパイル時の警告を無視しないこと。
2. 監視体制の構築: address(this).balance が急激に変動した場合にアラートを飛ばすスクリプトを走らせておくべきだ。以下は、Pythonで実装する簡易的な監視のヒントである。

# 監視スクリプトの断片(Web3.py利用)
from web3 import Web3

w3 = Web3(Web3.HTTPProvider('YOUR_RPC_URL'))
target_contract = "0x..." # 監視対象のコントラクトアドレス

def check_balance_anomaly(last_balance):
    current_balance = w3.eth.get_balance(target_contract)
    # 期待される残高変動以上の変化があれば通知する
    if abs(current_balance - last_balance) > EXPECTED_THRESHOLD:
        print("ALERT: 予期せぬ残高変動を検知!")
        # ここでSlackやPagerDutyへ飛ばす処理を実装する

—

まとめ:セキュリティの「盲点」を突く

Web3の世界では、「コードが法律」であるがゆえに、一度デプロイされたロジックの脆さは致命的だ。selfdestruct による強制送金は、Web2的なファイアウォールやWAFでは防げない。

  • 鉄則1: address(this).balance を require や計算のロジックに組み込むな。
  • 鉄則2: 常に内部変数で状態を管理せよ。
  • 鉄則3: ロジックが外部要因(強制送金)によって破壊される可能性を常に設計段階で考慮せよ。

泥臭い実装かもしれないが、これこそが「ハックされないシステム」を作るための唯一の道だ。きれいごとを並べるホワイトペーパーを読むよりも、こういった「攻撃者の視点」をコードに落とし込む姿勢を大切にしてほしい。現場からは以上だ。

コメント

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