Appearance
NotOptimisticPortal CTF 問題の詳細解析:クロスチェーンメッセージ検証におけるロジック脆弱性
概要
NotOptimisticPortal は、Optimism のクロスチェーンブリッジ機構を模倣した CTF 問題であり、一見安全に見えるメッセージ検証システムに重大な設計欠陥が存在することを示しています。本記事では、このコントラクトの動作原理、脆弱性、攻撃手法について詳しく解説し、クロスチェーン通信における典型的な落とし穴を理解します。
背景知識
クロスチェーンブリッジの基本原理
クロスチェーンブリッジは、異なるブロックチェーン間で資産や情報を移転する仕組みです。Optimism のような Layer 2 ソリューションでは、状態ルート(state root)を用いて Layer 2 上のトランザクションや状態変化を検証します。主な仕組みは以下の通りです:
- 状態ルートの提出:Sequencer が定期的に Layer 2 の状態ルートを Layer 1 に提出
- 不正証明 / 有効性証明:状態遷移の正当性を検証
- メッセージ伝達:Layer 1 と Layer 2 間で双方向通信を実現
Merkle Patricia Trie(MPT)
Ethereum は MPT を用いて状態データを効率的に保存・検証します。主な特徴:
- 各アカウントの状態(nonce、balance、storageRoot、codeHash)を状態ツリーに格納
- ストレージは個別のツリーで管理され、そのルートがアカウント状態に含まれる
- Merkle 証明により、特定データの存在証明が可能
コントラクト構造の分析
コアデータ構造
solidity
struct ProofData {
bytes stateTrieProof;
bytes storageTrieProof;
bytes accountStateRlp;
}
// L2 状態データ
bytes32 public latestBlockHash;
uint256 public latestBlockNumber;
uint256 public latestBlockTimestamp;
bytes32[MAX_ROOT_BUFFER] public l2StateRoots;
uint16 public bufferCounter;
mapping(bytes32 => bool) public executedMessages;
主要関数の分析
1. メッセージ実行フロー
executeMessage はクロスチェーンメッセージ処理の中核関数です:
solidity
function executeMessage(
address _tokenReceiver,
uint256 _amount,
address[] calldata _messageReceivers,
bytes[] calldata _messageData,
uint256 _salt,
ProofData calldata _proofs,
uint16 _bufferIndex
) external nonReentrant {
// 1. メッセージハッシュの計算と重複チェック
bytes32 withdrawalHash = _computeMessageSlot(...);
require(!executedMessages[withdrawalHash], "Message already executed");
// 2. メッセージ実行
for(uint256 i; i < _messageData.length; i++){
_executeOperation(_messageReceivers[i], _messageData[i], false);
}
// 3. 包含証明の検証
_verifyMessageInclusion(
withdrawalHash,
_proofs.stateTrieProof,
_proofs.storageTrieProof,
_proofs.accountStateRlp,
_bufferIndex
);
// 4. 状態更新とトークン発行
executedMessages[withdrawalHash] = true;
if(_amount != 0){
_mint(_tokenReceiver, _amount);
}
}
2. メッセージ検証機構
solidity
function _verifyMessageInclusion(
bytes32 messageSlot,
bytes calldata stateTrieProof,
bytes calldata storageTrieProof,
bytes calldata accountStateRlp,
uint16 bufferIndex
) internal view {
bool accountVerified = Lib_SecureMerkleTrie.verifyInclusionProof(
abi.encodePacked(L2_TARGET),
accountStateRlp,
stateTrieProof,
l2StateRoots[bufferIndex]
);
require(accountVerified, "Invalid account proof");
Lib_RLPReader.RLPItem[] memory accountState = accountStateRlp.toRLPItem().readList();
bytes32 storageRoot = accountState[2].readBytes32();
bool slotVerified = Lib_SecureMerkleTrie.verifyInclusionProof(
abi.encodePacked(messageSlot),
hex"01",
storageTrieProof,
storageRoot
);
require(slotVerified, "Invalid storage proof");
}
セキュリティ脆弱性の分析
脆弱性1:検証と実行の順序ミス
問題点:検証前に処理を実行している(CEI違反)
solidity
_executeOperation(...); // 先に実行
_verifyMessageInclusion(...); // 後で検証
リスク:無効なメッセージでも処理が実行される可能性
脆弱性2:関数セレクタ検証の不備
solidity
require(bytes4(callData[0:4]) == bytes4(0x3a69197e), "Invalid message entrypoint");
- これは
onMessageReceived(bytes)のセレクタ - しかし衝突可能なため不十分
脆弱性3:権限管理の欠陥
solidity
require(msg.sender == owner || msg.sender == address(this))
- コントラクト自身の呼び出しを許可
- 再入可能な権限昇格の入口になる
攻撃戦略
目的
- owner を奪取
- sequencer になる
- 任意の state root を提出
- 任意ミント
攻撃手順
ステップ1:悪意あるメッセージ構築
solidity
function onMessageReceived(bytes memory) override external {
require(msg.sender == address(_notOptimisticPortal));
require(_notOptimisticPortal.owner() == address(this));
_notOptimisticPortal.updateSequencer_____76439298743(address(this));
_notOptimisticPortal.submitNewBlock_____37278985983(maliciousHeader);
}
ステップ2:順序バグの悪用
solidity
msgReceivers[0] = portal;
msgReceivers[1] = attacker;
msgData[0] = transferOwnership(attacker);
msgData[1] = triggerAttack();
流れ:
- ownership 奪取
- 攻撃コントラクト呼び出し
- 検証は後(無意味)
ステップ3:検証回避
sequencer を取得後:
- 任意の state root 提出
- 任意の証明生成可能
技術詳細
RLP とブロックヘッダ
solidity
parentHash = header[0]
stateRoot = header[3]
number = header[8]
timestamp = header[11]
Merkle 検証
solidity
verifyInclusionProof(
key = messageSlot,
value = 0x01,
proof,
storageRoot
)
修正案
1. CEI 準拠
solidity
_verifyMessageInclusion(...);
_executeOperation(...);
2. セレクタ厳密化
solidity
require(
selector == IMessageReceiver.onMessageReceived.selector
);
3. 権限修正
solidity
require(msg.sender == owner);
4. タイムロック導入
solidity
scheduledOperations[id] = block.timestamp + 2 days;
まとめ
この問題はクロスチェーン設計の重要な教訓を示しています:
- 実行順序の重要性(CEI)
- 権限管理の厳格化
- 検証ロジックの信頼性確保
クロスチェーンブリッジは極めて高リスクな領域であり、小さな設計ミスでも致命的な被害につながります。したがって:
- 多層防御
- 厳格な検証
- 徹底した監査
が不可欠です。