由 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
地址不能为合约vat
和dog
的地址。原因是什么呢?
在 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.sender
为Clipper
合约的前提下,若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);
}
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!