FlashLoan 潜在利用点——特殊的权限访问控制漏洞

  • Q1ngying
  • 更新于 2024-07-17 14:06
  • 阅读 746

由 MakerDAO Liquidation2.0 部分 Clip 合约发散,一种特殊的,隐蔽的权限访问限制漏洞

潜在问题

在浏览 MakerDAO 源码的 Clipper 合约时,我观察到了这段代码:

function take(uint256 id, uint256 amt, uint256 max, uint256 who, bytes calldata data) {
    ...
    if (data.length > 0 && who != address(vat) && who != address(dog_)) {
        ClipperCallee(who).clipperCall(msg.sender, owe, slice, data);
    }
    vat.move(msg.sende, vow, oww);
    dog_.digs(ilk, lot == 0 ? tab + owe : owe);
    ...
}

我们观察这个函数,最主要的逻辑如上所示。可以看到,这个函数的函数名虽然是take,但是观察它的逻辑我们可以发现,这就是一个 FlashLoan 闪电贷函数。因为它是在外部调用之后才减少的msg.sender 的 DAI 余额。

不过观察这个函数执行外部调用的检测:

 if (data.length > 0 && who != address(vat) && who != address(dog_))

也就是说,who 地址不能为合约vatdog的地址。原因是什么呢?

在 MakerDAO 协议中,vat 合约和 dog 合约是这个协议关键的两个部分:

  • vat Vault 合约,MakerDAO 协议的核心,所有的数据都存储在 vat 合约中
  • dog Liquidation2.0 合约,负责 MakerDAO 协议的清算部分

vat 合约和 dog 合约都有严格的权限访问控制。因为其中充满了大量的系统参数,相关修改系统参数的函数都加以的权限控制(下面以vat举例):

mapping(address => uint256) public wards;
    function rely(address usr) external auth {
        require(live == 1, "Vat/not-live");
        wards[usr] = 1;
    }
    function deny(address usr) external auth {
        require(live == 1, "Vat/not-live");
        wards[usr] = 0;
    }
    modifier auth() {
        require(wards[msg.sender] == 1, "Vat/not-authorized");
        _;
    }
    function file(bytes32 what, uint256 data) external auth { // 修改系统参数
        require(live == 1, "Vat/not-live");
        if (what == "Line") Line = data;
        else revert("Vat/file-unrecognized-param");
    }

而对于 Clipper 合约来说,它是拥有 vat 合约和 dog 合约的授权的。也就是说,在msg.senderClipper合约的前提下,若calldata是一系列修改系统参数的函数调用,是可以成功调用的。

如果 Clipper 合约对于闪电贷模块(FlashLoan)的检测变为:

 if (data.length > 0 )

我们可以构造恶意函数调用,将参数 who 的地址设置为 vat 合约或者 dog 合约,data 中,构造修改系统参数file函数的calldata。实现成功修改系统参数,这对于后续的攻击有很大的可利用性。这是一种比较隐蔽的权限访问控制漏洞。

对于类似于闪电贷这种外部调用函数,加以严格的权限控制是十分必要的,然而在大篇幅的协议编码中,很容易忽略掉这部分,同样,这种错误通过 Fuzzing 貌似是无法捕捉到的。

所以对于 DeFi 协议或者任何外部调用的函数来说,加以详细的考究是十分必要的。

完整的take函数

完整 take 函数及注释:


// 从由 `id` 索引的拍卖中购买最多 `amt` 的抵押品。
    //
    // 拍卖不会收集比其指定的 DAI 目标 `tab` 更多的 DAI;
    // 因此,如果 `amt` 在当前价格下比 `tab` 花费更多的 DAI,则购买的抵押品数量将刚好足以收集 `tab` DAI。
    //
    // 为避免部分购买导致剩余拍卖非常少并且永远不会被清除,任何部分购买都必须至少留下
    // `Clipper.chost` 剩余的 DAI 目标。`chost` 是一个异步更新的值,等于
    // (Vat.dust * Dog.chop(ilk) / WAD),其中这些值被理解为由
    // 上次调用 Clipper.upchost() 时的值决定。购买金额将在必要时最小程度地减少以遵守此限制;
    // 即,如果指定的 `amt` 为 `tab < chost` 但 `tab > 0`,则实际购买的金额将为 `tab == chost`。
    //
    // 如果 `tab <= chost`,则不再可能进行部分购买;也就是说,剩余的
    // 抵押品只能全部购买,或者根本无法购买。
    /**
     * @notice 拍卖函数(拍抵押品的函数)
     * @param id 要拍的拍卖 id
     * @param amt 购买抵押品数量的上限 [wad]
     * @param max 最高可接受价格 (DAI / 抵押品) [ray]
     * @param who 抵押品接收者和外部调用地址(拍卖的地址)
     * @param data 传入的外部 calldata 数据,长度为 0 表示未进行调用
     * @notice
     * - `lock` 重入锁,`isStopped(3)` 断路器满足 < 3
     */
    function take(
        uint256 id, // Auction id
        uint256 amt, // Upper limit on amount of collateral to buy  [wad]
        uint256 max, // Maximum acceptable price (DAI / collateral) [ray]
        address who, // Receiver of collateral and external call address
        bytes calldata data // Data to pass in external call; if length 0, no call is done
    ) external lock isStopped(3) {
        address usr = sales[id].usr;
        uint96 tic = sales[id].tic;

        require(usr != address(0), "Clipper/not-running-auction");

        uint256 price;
        {
            bool done;
            (done, price) = status(tic, sales[id].top);

            // Check that auction doesn't need reset
            // 检测对应的拍卖是否需要重置
            require(!done, "Clipper/needs-reset");
        }

        // Ensure price is acceptable to buyer
        // 确保价格为买家所接受
        require(max >= price, "Clipper/too-expensive");

        uint256 lot = sales[id].lot;
        uint256 tab = sales[id].tab;
        uint256 owe;

        {
            // Purchase as much as possible, up to amt
            // 尽可能多的买,最多为 `amt`
            // 计算实际购买的抵押品数量,初始值为购买上限和拍卖中剩余抵押品数量中的最小值
            uint256 slice = min(lot, amt); // slice <= lot

            // DAI needed to buy a slice of this sale
            // 购买这笔拍卖的 slice 需要的 DAI
            owe = mul(slice, price);

            // Don't collect more than tab of DAI
            // 不收集超过 `tab` 目标的 DAI
            if (owe > tab) {
                // 如果所需 DAI 数量超过债务
                owe = tab; // owe' <= owe 调整所需 DAI 数量为债务数量
                // Adjust slice
                // 调整实际购买的抵押品数量
                slice = owe / price; // slice' = owe' / price <= owe / price == slice <= lot
            } else if (owe < tab && slice < lot) {
                // If slice == lot => auction completed => dust doesn't matter
                uint256 _chost = chost; // 获取最小剩余债务目标
                if (tab - owe < _chost) {
                    // safe as owe < tab 安全:因为 owe < tab
                    // If tab <= chost, buyers have to take the entire lot.
                    //  // 确保部分购买满足最小剩余债务目标
                    require(tab > _chost, "Clipper/no-partial-purchase");
                    // Adjust amount to pay
                    // 调整所需 DAI 数量
                    owe = tab - _chost; // owe' <= owe
                    // Adjust slice
                    // 调整实际购买抵押品数量
                    slice = owe / price; // slice' = owe' / price < owe / price == slice < lot
                }
            }

            // Calculate remaining tab after operation
            // 更新剩余债务
            tab = tab - owe; // safe since owe <= tab  安全:因为 owe <= tab
            // Calculate remaining lot after operation
            // 更新剩余抵押品数量
            lot = lot - slice;

            // Send collateral to who
            // 将抵押品发送给 `who`
            vat.flux(ilk, address(this), who, slice);

            // Do external call (if data is defined) but to be
            // extremely careful we don't allow to do it to the two
            // contracts which the Clipper needs to be authorized
            // 若 data 被定义,执行外部调用。不允许 `who` 为 `vat` 和 `dog`
            // vat 和 dog 对 Clipper 合约进行授权,所以限制用户,不能通过 low-level call
            // 调用 vat 和 dog 合约避免出现非预期问题,避免出现:权限访问控制漏洞
            DogLike dog_ = dog;
            if (data.length > 0 && who != address(vat) && who != address(dog_)) {
                ClipperCallee(who).clipperCall(msg.sender, owe, slice, data);
            }

            // Get DAI from caller
            // 从调用者处获取 DAI
            vat.move(msg.sender, vow, owe);

            // Removes Dai out for liquidation from accumulator
            // 从累加器中移除要清算的 DAI
            dog_.digs(ilk, lot == 0 ? tab + owe : owe);
        }

        if (lot == 0) {
            // 所有抵押品全部拍卖
            _remove(id); // 直接移除对应的拍卖
        } else if (tab == 0) {
            // lot != 0 => 抵押品剩余
            // 并且收取了目标 `tab` 的 DAI
            vat.flux(ilk, address(this), usr, lot); // 将剩余的抵押品转移回被清算者
            _remove(id); // 移除对应的拍卖
        } else {
            // 都不满足,证明拍卖还需要继续
            // 更新拍卖状态(剩余需要拍卖的目标,剩余的抵押品数量)
            sales[id].tab = tab;
            sales[id].lot = lot;
        }

        emit Take(id, max, price, owe, tab, lot, usr);
    }
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
Q1ngying
Q1ngying
0x468F...68bf
本科在读,合约安全学习中......