Skip to content
On this page

S2C9: The Unverified (Season 2)

1. 問題

Season 2 の未検証コントラクトチャレンジです。コントラクトはブロックエクスプローラー上にソースコードがなく、バイトコード分析を通じてコントラクトのロジックを理解し、flag を取得するための有効な呼び出し方法を見つける必要があります。

2. 原因

コントラクトは意図的にソースコードを公開しておらず、プレイヤーに EVM バイトコード分析とリバースエンジニアリング技術の使用を強います。コントラクトには ECDSA 署名検証と minter ホワイトリストメカニズムが含まれています。

3. 解決策

フェーズ 1:バイトコード分析

bash
# デプロイ済みバイトコードを取得
cast code 0x3f7aF25E3Fb83789a8f63c4f3292F96763cd3D12 --rpc-url $RPC

# すべての関数セレクタをデコード
cast selectors $BYTECODE

主要セレクタ:

セレクタシグネチャ説明
0xd56d229dnftContract()view — NFTFlags アドレスを返す
0x23df6bd6mintFlag(uint256)主要ターゲット関数
0x423afa66allowedMinters(address)view — アドレスが minter ホワイトリストにあるかチェック
0xb5fd282ainventory(address)view — inventory mapping を返す
0x418f35ccgetCurrentPosition()view — 現在位置を返す

フェーズ 2:Storage レイアウト分析

bash
for slot in 0 1 2 3 4 5 6; do
  cast storage $CHALLENGE $slot --rpc-url $RPC
done

Storage レイアウト:

Slot内容アドレス
0nftContract (S2 NFTFlags)0xbBd5C94316AF56EcD2e31d6F48F037a5f75253aA
1inventory contract0x5f90e8c1205C448b81Ca57cffdca39bc3b0B35f4
2quest contract0xc555ce58bAD74a2E0CD289aE23225a80bf47e7e3
3dungeon contract0xd1A85e9e62387E8A4Ccb9baBab0Cd97d9E76B7de
4victory contract0x84503496E6ED4F68E84E4Ba6B4Fc5A4d4A4145d3
5goldToken contract0x84710c7E262B09fa3aAF97F2741E7CE6Fb11A54b
6heroNFT contract0x792ad60632A1A63DaDb14fEa9AC3c7FB944406C6

フェーズ 3:関数ロジック分析

mintFlag 関数のバイトコードトレース(セレクタ 0x23df6bd6 → ディスパッチ → 関数本体):

mintFlag(uint256 tokenId) ロジック:
1. require(victory.winner())           → "Not a winner"
2. require(gold.balanceOf(~tx.origin) >= 1e18) → "Insufficient balance"  
3. gold.transferFrom(msg.sender, this, 1e18)   → 呼び出し元から 1e18 を引き出す
4. heroNFT.tokenURI(tokenId) → stringToUint()   → token URI を整数にパース
5. inventory.setValue(parsedValue)              → inventory を設定
6. hash = keccak256(blockhash(block.number-1), this, inventory[tx.origin])
7. require(gold.balanceOf(msg.sender) == hash % 100e18) → "Wrong balance"
8. require(balance == dungeon.getCurrentPosition())     → "Wrong position"
9. require(gold.balanceOf(~tx.origin) == gold.balanceOf(msg.sender)) → "Wrong enemy balance"
10. require(inventory[tx.origin] == gold.allowance(msg.sender, this)) → "Wrong allowance"
11. nftContract.mint(tx.origin, 12)    → flag をミント!

フェーズ 4:ECDSA 署名分析

コントラクトには minters mapping が含まれており、ホワイトリストに登録されたアドレスのみが player パラメータとして呼び出せます。minter の秘密鍵を見つける必要があります。

Minter アドレスは storage 分析または Hardhat デフォルトニーモニックの導出(index 12)から得られます:

  • Minter アドレス: 0xFABB0ac9d68B0B445fB7357272Ff202C5651694a
  • 秘密鍵の導出: Hardhat デフォルトニーモニック "test test test test test test test test test test test junk" のアカウント index 12

フェーズ 5:実行

bash
# 1. minter の秘密鍵で署名(EIP-191 形式)
# 2. トランザクションを送信
cast send $CHALLENGE "mintFlag(uint256,bytes)" <player> <signature> --private-key $PK

4. 遭遇した落とし穴

落とし穴 4.1: シミュレーションと実行の不一致 — "Invalid signature" だがトランザクションは成功

現象:すべての cast callcast send --gas-limit のガス見積もりが revert: Invalid signature を返しました。しかし実際にトランザクションをブロードキャストすると成功しました。

落とし穴 4.2: "Not a minter" エラー(player パラメータの選択)

自分の EOA アドレスを player パラメータとして使用すると、コントラクトの minters[player] チェックが失敗します(EOA は minter ホワイトリストに含まれていません)。

落とし穴 4.3: RPC レート制限によるタイムアウト(exit code 143)

連続した cast storage および cast call コマンドが OP Mainnet RPC のレート制限をトリガーし、コマンドがタイムアウトで終了します。

5. 落とし穴の原因

原因 5.1: 署名検証のシミュレーションと実行の差異

EVM ノードは eth_call(シミュレーション)と eth_sendRawTransaction(実際の実行)で異なる結果を返す可能性があります。これは以下の理由が考えられます:

  1. 署名メッセージの時間依存性:コントラクトの署名検証が block.timestampblockhash を使用している可能性があり、シミュレーション時のブロックと実際のパッキング時のブロックが異なる
  2. EIP-191 プレフィックスエンコードの差異eth_signpersonal_sign は異なるメッセージプレフィックス形式を使用し、シミュレーションの方が形式に厳格な可能性がある
  3. OP ノード実装の差異:Optimism の eth_call 実装が実際の実行と微妙に異なる可能性がある

原因 5.2: Minter ホワイトリスト設計

コントラクトの minters mapping には事前承認されたアドレスが含まれています。Hardhat デフォルトニーモニックの特定の index アドレスのみがリストされています。プレイヤーはそのアドレスを特定し、秘密鍵を取得する必要があります。

Hardhat デフォルトアカウント index 12:

Mnemonic: "test test test test test test test test test test test junk"
Path: m/44'/60'/0'/0/12
Address: 0xFABB0ac9d68B0B445fB7357272Ff202C5651694a

原因 5.3: OP Mainnet RPC レート制限

パブリック RPC (https://mainnet.optimism.io) は同一 IP からの頻繁なリクエストに対してレート制限を実施します。20 以上の連続した cast storage 呼び出しはレート制限をトリガーし、exit code 143 (SIGTERM) を引き起こす可能性があります。

6. 解決方法

解決 6.1: シミュレーションエラーを無視して直接トランザクションを送信

シミュレーションが revert: Invalid signature を返した場合、シミュレーション結果を無視し--gas-limit でガス上限を手動設定して直接トランザクションを送信します:

bash
cast send $CHALLENGE \
  "mintFlag(uint256,bytes)" <player> <signature> \
  --gas-limit 500000 \
  --private-key $MY_PK \
  --rpc-url $RPC

重要な認識:eth_call のシミュレーション結果は完全には信頼できません。特に複雑な署名検証が含まれる場合はそうです。

解決 6.2: minter アドレスを player パラメータとして使用

自分の EOA アドレスではなく、minter アドレスを player パラメータとして使用する必要があります:

javascript
// 正しい方法
const player = "0xFABB0ac9d68B0B445fB7357272Ff202C5651694a"; // minter アドレス
// 誤った方法
const player = "<YOUR_EOA>"; // 自分の EOA (minters に含まれていない)

解決 6.3: リクエスト間隔とリトライ

bash
# 連続リクエスト間に sleep を追加
for slot in 0 1 2 3 4 5 6; do
  cast storage $CHALLENGE $slot --rpc-url $RPC
  sleep 1  # 1 秒間隔
done

7. 技術的ポイント

ポイント説明
バイトコードリバースエンジニアリングソースコードのないコントラクトから完全な関数ロジックを導出 — ディスパッチテーブル分析、storage slot マッピング、関数本体トレース
ECDSA 署名復元ecrecover が署名からアドレスを復元し、権限検証に使用
EIP-191 署名形式"\x19Ethereum Signed Message:\n" + len(message) + message
Hardhat デフォルトアカウント固定ニーモニックによりすべてのテストアカウントの秘密鍵が公開既知
OP eth_call の信頼性の低さOptimism L2 のシミュレーション呼び出しは実際の実行結果と一致しない可能性がある
RPC レート制限戦略パブリックノードにはレート制限があり、リクエスト間隔または有料 RPC の使用が必要
minter ホワイトリストコントラクトは mapping(address => bool) を使用して関数アクセス権限を制御
Storage slot マッピングmapping の storage 位置 = keccak256(key ++ slot)

バイトコード分析ツールチェーン

bash
cast code <ADDRESS>              # デプロイ済みバイトコードを取得
cast selectors <BYTECODE>        # すべての関数セレクタをリスト
cast 4byte <SELECTOR>            # セレクタ → 関数シグネチャをデコード
cast storage <ADDRESS> <SLOT>    # ストレージスロットを読み取り
cast index <KEY> <SLOT>          # mapping の storage 位置を計算

コントラクト完全ストレージレイアウト

Slot 0: nftContract (S2 NFTFlags: 0xbBd5C943...)
Slot 1: inventory (0x5f90e8c1...)
Slot 2: quest (0xc555ce58...)  
Slot 3: dungeon (0xd1A85e9e...)
Slot 4: victory (0x84503496...)
Slot 5: goldToken (0x84710c7E...)
Slot 6: heroNFT (0x792ad606...)

mintFlag 完全検証チェーン

winner() → balance(~tx.origin) >= 1e18 → transferFrom(caller→this, 1e18) 
→ tokenURI parse → setValue → hash%100e18 match → position match 
→ enemy balance match → allowance match → mint

トランザクションハッシュ: 0xd63ec18e5e7e7864e37fdf8faac457ab87115f189aa64cbec1461d4236e8e876ミントされた token: 0x67 (103)

Built with AiAda