Skip to content
On this page

EllipticToken CTF 問題解析:ECDSA ドメイン混同脆弱性と署名偽造攻撃

背景紹介

ブロックチェーンセキュリティの分野において、楕円曲線デジタル署名アルゴリズム(ECDSA)は、トランザクションやメッセージの完全性を保護する中核的な技術です。しかし、開発者が「最適化」や独自改変を試みると、深刻なセキュリティ脆弱性を招くことがあります。本稿で分析する CTF 問題「Elliptic Token」は、その典型的な例を示しています。

EllipticToken($ETK)は ERC20 標準に基づくトークンコントラクトであり、楕円曲線署名を利用した2つの機能を実装しています:

  1. バウチャー引き換えシステム:コントラクト所有者 Bob がオフチェーンでバウチャーを発行し、ユーザーがオンチェーンで $ETK を受け取る
  2. パーミット機能:署名ベースの承認機構(ERC20 の permit に類似)

コントラクト構造の分析

コアデータ構造

solidity
contract EllipticToken is Ownable, ERC20 {
    mapping(bytes32 => bool) public usedHashes;
    
    // エラー定義
    error HashAlreadyUsed();
    error InvalidOwner();
    error InvalidReceiver();
    error InvalidSpender();
}

コントラクトは usedHashes マッピングを用いてリプレイ攻撃を防止し、各ハッシュが一度しか使われないようにしています。これは ECDSA ベースのシステムにおける一般的な対策です。


バウチャー引き換え機構

solidity
function redeemVoucher(
    uint256 amount,
    address receiver,
    bytes32 salt,
    bytes memory ownerSignature,
    bytes memory receiverSignature
) external {
    bytes32 voucherHash = keccak256(abi.encodePacked(amount, receiver, salt));
    require(!usedHashes[voucherHash], HashAlreadyUsed());

    // 所有者の署名検証
    require(ECDSA.recover(voucherHash, ownerSignature) == owner(), InvalidOwner());

    // 受取人の署名検証
    require(ECDSA.recover(voucherHash, receiverSignature) == receiver, InvalidReceiver());

    usedHashes[voucherHash] = true;
    _mint(receiver, amount);
}

この関数は比較的安全です。理由は以下の通り:

  1. パラメータ全体を用いてハッシュを生成している
  2. 2つの署名(所有者と受取人)を検証している
  3. ハッシュを使用済みとして記録している

問題箇所:permit 関数

solidity
function permit(uint256 amount, address spender, bytes memory tokenOwnerSignature, bytes memory spenderSignature)
    external
{
    bytes32 permitHash = keccak256(abi.encode(amount));
    require(!usedHashes[permitHash], HashAlreadyUsed());
    require(!usedHashes[bytes32(amount)], HashAlreadyUsed());

    // 重大な脆弱性
    address tokenOwner = ECDSA.recover(bytes32(amount), tokenOwnerSignature);

    bytes32 permitAcceptHash = keccak256(abi.encodePacked(tokenOwner, spender, amount));
    require(ECDSA.recover(permitAcceptHash, spenderSignature) == spender, InvalidSpender());

    usedHashes[permitHash] = true;
    _approve(tokenOwner, spender, amount);
}

脆弱性分析:ECDSA ドメイン混同攻撃

ECDSA 署名検証の基本

ECDSA 検証には以下の3要素が必要です:

  1. メッセージハッシュ
  2. 署名(r, s, v)
  3. 復元される公開鍵(アドレス)

OpenZeppelin の ECDSA.recover は「ハッシュ化済みのメッセージ」を前提としています。


脆弱性の本質

solidity
address tokenOwner = ECDSA.recover(bytes32(amount), tokenOwnerSignature);

ここでは amount(uint256)をそのまま bytes32 に変換し、「ハッシュ」として扱っています。しかし実際にはこれは単なる整数であり、正規のハッシュではありません。


攻撃ベクトル

攻撃者は次のように行動できます:

  1. bytes32(amount) が既存の有効な署名のメッセージハッシュと一致するような値を見つける
  2. 被害者(例:Alice)の既存署名を利用
  3. それを「amount に対する署名」として再利用

数学的背景

ECDSA の署名生成:

  • r = (k × G).x mod n
  • s = k⁻¹ × (h + r × d) mod n

ここで攻撃者が h(=bytes32(amount))を制御できるため:

  1. 既知の署名 (r, s, v) を取得
  2. 対応するハッシュ h' を特定
  3. amount = uint256(h') に設定
  4. 同じ署名で検証を通過

攻撃手順

ステップ1:Alice のトランザクション分析

以下を調査:

  • redeemVoucher 呼び出し
  • その他署名付きトランザクション

ステップ2:署名の抽出

solidity
bytes32 voucherHash = keccak256(abi.encodePacked(amount, receiver, salt));

Alice の署名はこのハッシュに対するもの。


ステップ3:攻撃パラメータ構築

solidity
bytes32 Ar = 0x6690...;
bytes32 As = 0x3e38...;
uint8 Av = 27;

amount = uint256(0xfd88...);

ALICE_new_sig = abi.encodePacked(Ar, As, Av);

ステップ4:攻撃実行

solidity
EllipticToken(ELLIPTICTOKEN_INST).permit(amount, player, ALICE_new_sig, sig);

EllipticToken(ELLIPTICTOKEN_INST).transferFrom(
    ALICE, 
    player, 
    EllipticToken(ELLIPTICTOKEN_INST).balanceOf(ALICE)
);

修正方法

修正案1:正しいハッシュ使用

solidity
bytes32 permitHash = keccak256(abi.encodePacked(
    "PERMIT", 
    amount, 
    spender,
    block.chainid,
    address(this)
));

修正案2:EIP-712 の導入

solidity
bytes32 digest = keccak256(
    abi.encodePacked(
        "\x19\x01",
        domainSeparator,
        structHash
    )
);

セキュリティ対策

  1. 生データに直接署名しない
  2. EIP-712 を使用する
  3. nonce によるリプレイ防止
  4. セキュリティ監査の実施
  5. 信頼済みライブラリの利用

まとめ

EllipticToken の脆弱性は、ECDSA における「ドメイン混同」の危険性を示しています。bytes32(amount) をハッシュとして扱う誤りにより、既存署名の再利用が可能となり、不正な承認が成立してしまいます。

この問題は、暗号実装において標準を遵守する重要性を強く示しています。

覚えておくべき教訓:楕円曲線はドメイン混同を許さない。

Built with AiAda