銀行の金庫番を騙す「泥棒の二度漬け」?再入攻撃を徹底攻略しよう
こんにちは!セキュリティリサーチャーの現場から、今日は少しだけ「背筋が凍る」お話をします。
ブロックチェーンの世界で「再入攻撃(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)を活用する
これらを守るだけで、あなたのプログラムは格段に硬くなります。
最初は難しく感じるかもしれませんが、大丈夫。一つひとつのコードが「家の鍵」だと思って、丁寧に向き合っていきましょう。もし分からないことがあれば、またいつでも聞きに来てくださいね!一緒に安全なデジタル社会を作っていきましょう。
コメント