Appearance
S1C5: Give Me My Points!
1. 問題
コントラクトにはポイントシステムがあります:claimPoints() は毎回 +1 ポイント、upgradeLevel() はポイントを消費してレベルアップします。flag をミントするには特定の条件(ポイント × レベル)を満たす必要があります。コントラクトは単一の points 変数を使用します。
2. 原因
claimPoints() 関数は、points の状態を更新する前に外部呼び出し(ETHの送金)を行います。これにより、攻撃者は receive() コールバックを通じて claimPoints() に再入し、状態が更新される前に繰り返しポイントを取得できます。
3. 方案
攻撃用コントラクトをデプロイし、リエントランシー攻撃を利用します:
solidity
contract S1C5Solution {
function solve() external {
challenge.claimPoints(); // リエントランシーループを開始
// ポイントが十分に蓄積された
challenge.upgradeLevel();
challenge.mintFlag();
}
receive() external payable {
if (points < TARGET) {
challenge.claimPoints(); // 再入
}
}
}
4. 遭遇したトラップ
リエントランシー回数の制御: receive() 内で再入回数を正確に制御し、ガス切れを防ぐ必要があります。
5. トラップの原因
再入のたびにガスが消費されます。ループ回数が多すぎると、ブロックのガスリミットを超える可能性があります。ループ内にカウンターを追加し、目標回数に達したら停止する必要があります。
6. 解決方法
攻撃用コントラクト内でカウンター変数を使用し、receive() 内でカウンターをチェックして目標回数に達したら再入を停止します。
7. 技術ポイント
| ポイント | 説明 |
|---|---|
| リエントランシー攻撃 (Reentrancy) | 外部呼び出しが状態更新の前に実行されることで、攻撃者が同じ関数に繰り返し入ることが可能になる |
| CEI パターン | Checks-Effects-Interactions:最初に状態を更新し、その後外部呼び出しを行うべき |
| ガス管理 | リエントランシーループはガスリミット超過を避けるため回数を制御する必要がある |