【ブロックチェーン】知るだけで防げる!Reentrancy攻撃とは?【セキュリティ】
Reentrancy(リエントランシー)攻撃の基礎と対策
キミはブロックチェーンの世界に潜む「再入(リエントランシー)攻撃」を知っているかい?
Ethereum が Ethereal Classic と分かれる原因になったのは、この再入攻撃が大きな原因になったんだ。
現在でも脅威となっている攻撃だが、単純な対策で防げるので是非とも学んでおきたい。
この記事では、スマートコントラクトを例にこの脆弱性を解説し、資産を守るための学びを共有するぞ。難しい言葉を避け、初心者でも理解できるようにまとめたので、安心して読み進めてくれ。
1. 事件や概念の紹介
・何が起きたか/どんな技術か
スマートコントラクトはブロックチェーン上でプログラムによる自動処理を行う技術です。その中でもイーサリアム系のコントラクトでは、「外部コントラクトへの呼び出し」が行われます。この際、関数の途中で外部コントラクトから再び元の関数を呼び出す(再入)ことが可能で、これを悪用した攻撃をReentrancy(リエントランシー)攻撃と呼びます 。
以下、Reentrancy攻撃は「再入攻撃」と表記します。
有名な実例として、2016年のDAO事件があります。資金調達用のスマートコントラクトが再入攻撃を受け、約360万ETHが盗まれ、当時の価格で約68億ドルに相当する損失となりました 。また、2022年4月にはFei Protocolのスマートコントラクトがチェック・エフェクト・インタラクション(以下「CEI」と表記)の手順を守っていなかったため、攻撃者に約8,000万ドル相当のトークンを盗まれました 。
2024年だけでもブロックチェーン関連の149件の事件で総額14.2億ドルが失われ、そのうち再入攻撃による損失は3,570万ドルに上りました 。こうした事例が示すように、再入攻撃は現在でも深刻な脅威です。
補足:なぜEthereum からEthereum Classic が分かれたのか
上記で触れた、2016年のDAO事件が直接の原因となっています。
当時、資金を失った投資家を救済するために「ハードフォークして盗まれた資金を巻き戻すか?」という議論がEthereumコミュニティで発生しました。
議論は紛糾し、2つの派閥に分裂。
- ハードフォーク派(現在のETH)
「投資家保護のために取引を巻き戻すべき」という意見を支持し、ハードフォークを実施。
- 反対派(現在のETC)
「ブロックチェーンは不変であるべき。過去を改ざんするのは理念に反する」という立場を取り、フォークせずに元のチェーンを維持。
議論の結果、現在は以下の2つの通貨に分かれてそれぞれ運用されています。
- Ethereum (ETH) = ハードフォーク後のチェーン(大多数が支持)。
- Ethereum Classic (ETC) = オリジナルのチェーン(少数派が支持)。
因みに私は、ブロックチェーンは不変であるべきと考えているので、ETCの立場に立っていたと思う。
2. 技術的な仕組み
・脆弱性のコード例
再入攻撃が起きるのは、スマートコントラクトが状態(残高など)を更新する前に外部コントラクトへ資金を送るためです。以下はOWASPが示す脆弱なSolidityコードの例です 。
contract VulnerableBank {
mapping(address => uint) public balances;
function deposit() external payable {
balances[msg.sender] += msg.value;
}
function withdraw() external {
uint amount = balances[msg.sender];
require(amount > 0, "Insufficient balance");
// 脆弱性: ETHを送金してから残高を0にしている
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
// 残高の更新が後回しになっているため再入できてしまう
balances[msg.sender] = 0;
}
}このコードでは、withdraw 関数が送金処理を先に行い、その後に残高を0にしています。このタイミングで攻撃者のコントラクトがfallback関数やreceive関数を使って再びwithdrawを呼び出すと、まだ残高が減っていないため繰り返し資金を引き出すことができます 。QuickNodeの解説によると、攻撃者はまず少額のETHをdepositし、その後コントラクトに連続で再入することで被害者の資金が尽きるまで引き出し続けます 。
・攻撃の流れ
- 預金 – 攻撃者は被害者コントラクトに最低限のトークンを預けます。
- 初回の引き出し – 攻撃者は脆弱なwithdraw関数を呼び出し、ETHやトークン)を引き出します。この時点では残高がまだ更新されていません。
- 再入 – 攻撃者コントラクトのfallback/receive関数が呼ばれ、ここから再び同じwithdraw関数を呼び出します。元のコントラクトはまだ残高を更新していないため、二度目の呼び出しでも資金を返してしまいます。
- ループ – 攻撃者はこれを繰り返し、コントラクトの資金が枯渇するまで引き出し続けます 。
この攻撃は単一の関数だけで起きるとは限りません。複数の関数が同じ状態変数を操作するクロス関数再入や、複数のコントラクトをまたいで再入が起きるクロスコントラクト再入、ライブラリなどのdelegatecallを悪用する委任再入などのバリエーションも存在します 。複雑なDeFiプロトコルではこれらが組み合わさることで攻撃経路が増え、被害が拡大するのです。
・安全なコード例
再入攻撃を防ぐには、状態の更新を先に行うというCEIパターンを守ることが重要です。下記は脆弱な例を修正したバージョンです 。
contract SafeBank {
mapping(address => uint) public balances;
function deposit() external payable {
balances[msg.sender] += msg.value;
}
function withdraw() external {
uint amount = balances[msg.sender];
require(amount > 0, "Insufficient balance");
// 残高を先にゼロにしてから送金する
balances[msg.sender] = 0;
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
}
}また、OpenZeppelinのReentrancyGuardを用いて関数にnonReentrant修飾子を付けることで、同じ取引中に再びその関数が呼ばれることを防ぐことができます。さらに、外部コントラクトのアドレスを信頼できるもののみに限定する、sendやtransferではなく安全な低レベル呼び出しを使うなどの対策も有効です。
3. なぜ問題になるか
再入攻撃はユーザーの資産を直接奪い、プロジェクトの信用を失墜させます。DAO事件では約360万ETHが流出し、Ethereumがハードフォークするほどの混乱を招きました 。Fei Protocolでは約8,000万ドル相当の資産が失われ 、Rari Capitalでは2021年にdYdXで借りた資金を利用して繰り返し引き出し、2,600 ETH以上を数時間で持ち去られました 。2024年の統計では、再入攻撃は149件のブロックチェーン事件の中で上位の攻撃ベクトルとなり、総被害額14.2億ドルのうち約3,570万ドルが再入による損失でした 。これらの被害は利用者への直接的な経済損失だけでなく、プロジェクトへの信頼低下や市場全体の信用不安にもつながります。
4. 対策・学び
- チェック・エフェクト・インタラクション(CEI)パターンの徹底 – コントラクト内の状態を更新してから外部呼び出しを行いましょう。Fei ProtocolのハックはCEIの順序を守らなかったことが原因で発生しました 。
- 再入防止のモジュール利用 – OpenZeppelinのReentrancyGuardを導入し、外部からの再入をブロックします。これにより、単一関数およびクロス関数の再入が防止されます 。
- 承認された外部コントラクトのみを利用 – 信頼できないコントラクトへは直接呼び出さないようにし、必要であればアドレスリストによるホワイトリストを実装します。
- セキュリティ監査とテスト – Fei Protocol事件では、コードがCompoundからフォークされていながら監査が行われず、古い脆弱性が残っていました 。デプロイ前に専門家による監査やフォーマル検証を実施し、再入の痕跡がないか確認しましょう。
- 限度額や時間制限の設定 – 一度に引き出せる金額や回数を制限することで、万一再入攻撃を受けても被害を抑えることができます。
5. まとめ
再入攻撃はスマートコントラクトの基本的なミス(状態の更新より先に外部への送金を行うこと)から生まれます 。DAO事件に始まり、Fei ProtocolやRari Capitalなど多くのプロジェクトが同じ過ちを繰り返してきました。
幸いにも、CEIパターンの徹底やReentrancyGuardの使用など、対策は比較的シンプルです。
あなたも、コードを書くときは「まず内部状態を更新し、それから外部とやりとりする」という基本を忘れないでください。そうすれば、再入の魔の手からあなたの資産やプロジェクトを守ることができるでしょう。