【入門編】 再入攻撃(Reentrancy)の高度な変種とマルチコントラクト間攻撃 – IoT・OT(制御システム) & ブロックチェーンセキュリティ防御ガイド

銀行の金庫番を騙す「泥棒の二度漬け」?再入攻撃を徹底攻略しよう

こんにちは!セキュリティリサーチャーの現場から、今日は少しだけ「背筋が凍る」お話をします。

ブロックチェーンの世界で「再入攻撃(Reentrancy Attack)」という名前を聞いたことはありませんか?これ、実はものすごく単純で、かつ最高にタチの悪い泥棒の手口なんです。

今日は、難しい専門用語を一旦脇に置いて、私たちが普段暮らしている「家の防犯」に例えながら、この攻撃の正体と、それを防ぐための「最強の鉄則」を一緒に学んでいきましょう!

—

1. 再入攻撃って、何が怖いの?

まずは想像してみてください。あなたは銀行の窓口担当者です。お客さんが「預けたお金を引き出したい」と言ってきました。

1. 残高確認: 窓口担当が通帳を見て「お、100万円あるな」と確認します。
2. 支払い: 100万円を現金で渡します。
3. 記録更新: 「よし、残高を0円に書き換えよう」とペンを握ります。

ここまでは完璧ですよね?でも、もし「お金を受け取ったその瞬間に、お客さんが窓口のベルを連打して、もう一度『お金をください!』と叫んだら」どうなるでしょう?

窓口担当はまだ「残高を0円にする」という作業を終えていません。だから、通帳をもう一度見ると、そこにはまだ「100万円」と書かれているのです。結果、銀行はもう100万円を支払ってしまう……。これが「再入攻撃」のメカニズムです。

なぜこれが「マルチコントラクト」で凶悪になるのか?

単一のコントラクト(金庫)ならまだしも、現代のWeb3では複数の金庫が連携しています。Aという銀行からお金を引き出し、その隙にBという別の銀行のルールを書き換え、またAに戻ってきて……という「コンボ攻撃」を仕掛けられると、被害は一瞬で数億円規模に膨れ上がります。

—

2. 泥棒を追い返すための「鉄則」

この攻撃を防ぐのは意外とシンプルです。「仕事の順番」を間違えないこと、これに尽きます。

セキュリティ業界では、これを Checks-Effects-Interactions(確認・更新・操作) パターンと呼んでいます。家の鍵に例えるなら、「ドアを開ける(操作)」前に、「誰が来たか確認し(確認)」、「中に入れるかどうか決める(更新)」というルールです。

悪い例(攻撃されやすいコード)

// 危険!先に「外部への支払い」をしてから「残高更新」をしています
function withdraw(uint _amount) public {
    require(balances[msg.sender] >= _amount);
    
    // 1. お金を送る(ここが攻撃の隙間!)
    (bool success, ) = msg.sender.call{value: _amount}("");
    require(success);
    
    // 2. 残高を減らす(遅すぎる!)
    balances[msg.sender] -= _amount;
}

良い例(「鉄則」を守ったコード)

// 安全!先に「残高」をゼロにしてから、最後にお金を送る
function withdraw(uint _amount) public {
    // 1. 確認(Checks)
    require(balances[msg.sender] >= _amount);
    
    // 2. 更新(Effects)- 先にゼロにする!
    balances[msg.sender] -= _amount;
    
    // 3. 操作(Interactions)- 最後に送る
    (bool success, ) = msg.sender.call{value: _amount}("");
    require(success);
}

「先に残高を引く」ことで、泥棒がベルを連打しても、「おっと、あなたの残高はもう0ですよ」と冷たくあしらうことができるようになるんです。

—

3. さらに強固な「鍵」をかける:ReentrancyGuard

コードの順番を守るだけでなく、現代のセキュリティでは「物理的な鍵」もかけます。OpenZeppelinという世界標準のライブラリにある nonReentrant という機能(修飾子)を使うのが一般的です。

// インポートして設定するだけで、二重アクセスを物理的にブロックします
import "@openzeppelin/contracts/security/ReentrancyGuard.sol";

contract MyBank is ReentrancyGuard {
    // この関数には「入室中」という札が掲げられ、
    // 処理が終わるまで他の人は入れなくなります
    function withdraw(uint _amount) public nonReentrant {
        require(balances[msg.sender] >= _amount);
        balances[msg.sender] -= _amount;
        (bool success, ) = msg.sender.call{value: _amount}("");
        require(success);
    }
}

この nonReentrant というのは、言わば「トイレのドアの鍵」です。誰かが入っている間は、外からどれだけドアノブをガチャガチャ(再入)しようとしても、絶対に開かない仕組みを作ってくれるのです。

—

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

Web3の開発では、自分の書いたコードが「他の誰か(あるいは他のコントラクト)によって、意図しないタイミングで叩かれる可能性がある」と常に疑うことが大切です。

  • Checks-Effects-Interactions を守る
  • 最新の防犯ライブラリ(ReentrancyGuard)を活用する

これらを守るだけで、あなたのプログラムは格段に硬くなります。

最初は難しく感じるかもしれませんが、大丈夫。一つひとつのコードが「家の鍵」だと思って、丁寧に向き合っていきましょう。もし分からないことがあれば、またいつでも聞きに来てくださいね!一緒に安全なデジタル社会を作っていきましょう。

コメント

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