{"content":{"title":"FlashLoan 潜在利用点——特殊的权限访问控制漏洞","body":"## 潜在问题\r\n在浏览 MakerDAO 源码的 `Clipper` 合约时，我观察到了这段代码：\r\n\r\n```solidity\r\nfunction take(uint256 id, uint256 amt, uint256 max, uint256 who, bytes calldata data) {\r\n\t...\r\n    if (data.length > 0 && who != address(vat) && who != address(dog_)) {\r\n        ClipperCallee(who).clipperCall(msg.sender, owe, slice, data);\r\n    }\r\n    vat.move(msg.sende, vow, oww);\r\n    dog_.digs(ilk, lot == 0 ? tab + owe : owe);\r\n    ...\r\n}\r\n```\r\n\r\n我们观察这个函数，最主要的逻辑如上所示。可以看到，这个函数的函数名虽然是`take`，但是观察它的逻辑我们可以发现，这就是一个 `FlashLoan` 闪电贷函数。因为它是在外部调用之后才减少的`msg.sender` 的 DAI 余额。\r\n\r\n不过观察这个函数执行外部调用的检测：\r\n\r\n```solidity\r\n if (data.length > 0 && who != address(vat) && who != address(dog_))\r\n```\r\n\r\n也就是说，`who` 地址不能为合约`vat`和`dog`的地址。原因是什么呢？\r\n\r\n在 MakerDAO 协议中，`vat` 合约和 `dog` 合约是这个协议关键的两个部分：\r\n\r\n- `vat` Vault 合约，MakerDAO 协议的核心，所有的数据都存储在 `vat` 合约中\r\n- `dog` Liquidation2.0 合约，负责 MakerDAO 协议的清算部分\r\n\r\n`vat` 合约和 `dog` 合约都有严格的权限访问控制。因为其中充满了大量的系统参数，相关修改系统参数的函数都加以的权限控制（下面以`vat`举例）：\r\n\r\n```solidity\r\nmapping(address => uint256) public wards;\r\n    function rely(address usr) external auth {\r\n        require(live == 1, \"Vat/not-live\");\r\n        wards[usr] = 1;\r\n    }\r\n    function deny(address usr) external auth {\r\n        require(live == 1, \"Vat/not-live\");\r\n        wards[usr] = 0;\r\n    }\r\n    modifier auth() {\r\n        require(wards[msg.sender] == 1, \"Vat/not-authorized\");\r\n        _;\r\n    }\r\n\tfunction file(bytes32 what, uint256 data) external auth { // 修改系统参数\r\n        require(live == 1, \"Vat/not-live\");\r\n        if (what == \"Line\") Line = data;\r\n        else revert(\"Vat/file-unrecognized-param\");\r\n    }\r\n```\r\n\r\n而对于 `Clipper` 合约来说，它是拥有 `vat` 合约和 `dog` 合约的授权的。**也就是说，在`msg.sender`为`Clipper`合约的前提下，若`calldata`是一系列修改系统参数的函数调用，是可以成功调用的。**\r\n\r\n如果 `Clipper` 合约对于闪电贷模块（FlashLoan）的检测变为：\r\n\r\n```solidity\r\n if (data.length > 0 )\r\n```\r\n\r\n我们可以构造恶意函数调用，将参数 `who` 的地址设置为 `vat` 合约或者 `dog` 合约，`data` 中，构造修改系统参数`file`函数的`calldata`。实现成功修改系统参数，这对于后续的攻击有很大的可利用性。**这是一种比较隐蔽的权限访问控制漏洞。**\r\n\r\n对于类似于闪电贷这种外部调用函数，加以严格的权限控制是十分必要的，然而在大篇幅的协议编码中，很容易忽略掉这部分，同样，这种错误通过 Fuzzing 貌似是无法捕捉到的。\r\n\r\n所以对于 DeFi 协议或者任何外部调用的函数来说，加以详细的考究是十分必要的。\r\n\r\n## 完整的`take`函数\r\n完整 `take` 函数及注释：\r\n```solidity\r\n// 从由 `id` 索引的拍卖中购买最多 `amt` 的抵押品。\r\n    //\r\n    // 拍卖不会收集比其指定的 DAI 目标 `tab` 更多的 DAI；\r\n    // 因此，如果 `amt` 在当前价格下比 `tab` 花费更多的 DAI，则购买的抵押品数量将刚好足以收集 `tab` DAI。\r\n    //\r\n    // 为避免部分购买导致剩余拍卖非常少并且永远不会被清除，任何部分购买都必须至少留下\r\n    // `Clipper.chost` 剩余的 DAI 目标。`chost` 是一个异步更新的值，等于\r\n    // (Vat.dust * Dog.chop(ilk) / WAD)，其中这些值被理解为由\r\n    // 上次调用 Clipper.upchost() 时的值决定。购买金额将在必要时最小程度地减少以遵守此限制；\r\n    // 即，如果指定的 `amt` 为 `tab < chost` 但 `tab > 0`，则实际购买的金额将为 `tab == chost`。\r\n    //\r\n    // 如果 `tab <= chost`，则不再可能进行部分购买；也就是说，剩余的\r\n    // 抵押品只能全部购买，或者根本无法购买。\r\n    /**\r\n     * @notice 拍卖函数（拍抵押品的函数）\r\n     * @param id 要拍的拍卖 id\r\n     * @param amt 购买抵押品数量的上限 [wad]\r\n     * @param max 最高可接受价格 (DAI / 抵押品) [ray]\r\n     * @param who 抵押品接收者和外部调用地址（拍卖的地址）\r\n     * @param data 传入的外部 calldata 数据，长度为 0 表示未进行调用\r\n     * @notice\r\n     * - `lock` 重入锁，`isStopped(3)` 断路器满足 < 3\r\n     */\r\n    function take(\r\n        uint256 id, // Auction id\r\n        uint256 amt, // Upper limit on amount of collateral to buy  [wad]\r\n        uint256 max, // Maximum acceptable price (DAI / collateral) [ray]\r\n        address who, // Receiver of collateral and external call address\r\n        bytes calldata data // Data to pass in external call; if length 0, no call is done\r\n    ) external lock isStopped(3) {\r\n        address usr = sales[id].usr;\r\n        uint96 tic = sales[id].tic;\r\n\r\n        require(usr != address(0), \"Clipper/not-running-auction\");\r\n\r\n        uint256 price;\r\n        {\r\n            bool done;\r\n            (done, price) = status(tic, sales[id].top);\r\n\r\n            // Check that auction doesn't need reset\r\n            // 检测对应的拍卖是否需要重置\r\n            require(!done, \"Clipper/needs-reset\");\r\n        }\r\n\r\n        // Ensure price is acceptable to buyer\r\n        // 确保价格为买家所接受\r\n        require(max >= price, \"Clipper/too-expensive\");\r\n\r\n        uint256 lot = sales[id].lot;\r\n        uint256 tab = sales[id].tab;\r\n        uint256 owe;\r\n\r\n        {\r\n            // Purchase as much as possible, up to amt\r\n            // 尽可能多的买，最多为 `amt`\r\n            // 计算实际购买的抵押品数量，初始值为购买上限和拍卖中剩余抵押品数量中的最小值\r\n            uint256 slice = min(lot, amt); // slice <= lot\r\n\r\n            // DAI needed to buy a slice of this sale\r\n            // 购买这笔拍卖的 slice 需要的 DAI\r\n            owe = mul(slice, price);\r\n\r\n            // Don't collect more than tab of DAI\r\n            // 不收集超过 `tab` 目标的 DAI\r\n            if (owe > tab) {\r\n                // 如果所需 DAI 数量超过债务\r\n                owe = tab; // owe' <= owe 调整所需 DAI 数量为债务数量\r\n                // Adjust slice\r\n                // 调整实际购买的抵押品数量\r\n                slice = owe / price; // slice' = owe' / price <= owe / price == slice <= lot\r\n            } else if (owe < tab && slice < lot) {\r\n                // If slice == lot => auction completed => dust doesn't matter\r\n                uint256 _chost = chost; // 获取最小剩余债务目标\r\n                if (tab - owe < _chost) {\r\n                    // safe as owe < tab 安全：因为 owe < tab\r\n                    // If tab <= chost, buyers have to take the entire lot.\r\n                    //  // 确保部分购买满足最小剩余债务目标\r\n                    require(tab > _chost, \"Clipper/no-partial-purchase\");\r\n                    // Adjust amount to pay\r\n                    // 调整所需 DAI 数量\r\n                    owe = tab - _chost; // owe' <= owe\r\n                    // Adjust slice\r\n                    // 调整实际购买抵押品数量\r\n                    slice = owe / price; // slice' = owe' / price < owe / price == slice < lot\r\n                }\r\n            }\r\n\r\n            // Calculate remaining tab after operation\r\n            // 更新剩余债务\r\n            tab = tab - owe; // safe since owe <= tab  安全：因为 owe <= tab\r\n            // Calculate remaining lot after operation\r\n            // 更新剩余抵押品数量\r\n            lot = lot - slice;\r\n\r\n            // Send collateral to who\r\n            // 将抵押品发送给 `who`\r\n            vat.flux(ilk, address(this), who, slice);\r\n\r\n            // Do external call (if data is defined) but to be\r\n            // extremely careful we don't allow to do it to the two\r\n            // contracts which the Clipper needs to be authorized\r\n            // 若 data 被定义，执行外部调用。不允许 `who` 为 `vat` 和 `dog`\r\n            // vat 和 dog 对 Clipper 合约进行授权，所以限制用户，不能通过 low-level call\r\n            // 调用 vat 和 dog 合约避免出现非预期问题，避免出现：权限访问控制漏洞\r\n            DogLike dog_ = dog;\r\n            if (data.length > 0 && who != address(vat) && who != address(dog_)) {\r\n                ClipperCallee(who).clipperCall(msg.sender, owe, slice, data);\r\n            }\r\n\r\n            // Get DAI from caller\r\n            // 从调用者处获取 DAI\r\n            vat.move(msg.sender, vow, owe);\r\n\r\n            // Removes Dai out for liquidation from accumulator\r\n            // 从累加器中移除要清算的 DAI\r\n            dog_.digs(ilk, lot == 0 ? tab + owe : owe);\r\n        }\r\n\r\n        if (lot == 0) {\r\n            // 所有抵押品全部拍卖\r\n            _remove(id); // 直接移除对应的拍卖\r\n        } else if (tab == 0) {\r\n            // lot != 0 => 抵押品剩余\r\n            // 并且收取了目标 `tab` 的 DAI\r\n            vat.flux(ilk, address(this), usr, lot); // 将剩余的抵押品转移回被清算者\r\n            _remove(id); // 移除对应的拍卖\r\n        } else {\r\n            // 都不满足，证明拍卖还需要继续\r\n            // 更新拍卖状态（剩余需要拍卖的目标，剩余的抵押品数量）\r\n            sales[id].tab = tab;\r\n            sales[id].lot = lot;\r\n        }\r\n\r\n        emit Take(id, max, price, owe, tab, lot, usr);\r\n    }"},"author":{"user":"https://learnblockchain.cn/people/16222","address":"0x468FA4c23c94012bf170207210d26E02a8a268bf"},"history":"bafkreigr2jcpdtqobw75j3nt3rt3tbf6utsa4l7yn3b5ykiulgp6dynke4","timestamp":1721196393,"version":1}