由于微信限制了第三方应用的跳转,请使用以下方法。
1. 点击右上角的
2. 选择在浏览器中打开
文章转载来源: ZAN Team
2024 年 3 月 28 日,Prisma Finance 遭遇攻击,目前累计亏损约 1100 万美元。攻击发生后,Prisma Finance 紧急暂停了项目,并告知用户赶紧取消委托授权(https://twitter.com/PrismaFi/status/1773371030129524957)。
Prisma Finance 是一个非托管、去中心化的,以抵押以太坊 LST(流动性质押代币)铸造稳定币的项目。比如用户可以通过抵押wstETH来铸造mkUSD,这个过程可以创建一个 trove,trove 可以理解为是一个记录指定 borrower 的抵押借贷情况的宝库,这个宝库有一个 trove manager,用来管理宝库的抵押物以及借贷币(铸造的稳定币)。
本次的漏洞合约是 MigrateTroveZap 合约,该合约的主要功能是将用户的抵押物从一个 trove manager 迁移到另一个 trove manager。因此创建 trove 的 borrower 就可能会授权 MigrateTroveZap 合约对其 trove 进行操作。然而 MigrateTroveZap 合约中的onFlashloan函数缺乏输入验证,从而允许攻击者通过 MigrateTroveZap 合约操作其他 borrower 的 trove,将其他 borrower 的 trove 中的抵押品数量变少,但是债务基本不变,从而盗取其他 borrower 的抵押品。
本次攻击涉及到多笔交易,我们仅以下面这比交易为例来对攻击进行分析。
攻击交易:https://etherscan.io/tx/0xe15fa959627871845f2f5bbfbd7529e6d2aff20ab14ece743f11641700bd7188
攻击 EOA:
0x7e39e3b3ff7adef2613d5cc49558eab74b9a4202
攻击者(合约):
0xd996073019c74b2fb94ead236e32032405bc027c
受害者(Prisma Finance TroveManager):
0x1cc79f3f47bfc060b6f761fcd1afc6d399a968b6
漏洞合约(Prisma Finance MigrateTroveZap):
0xcc7218100da61441905e0c327749972e3cbee9ee
一个被利用的 borrower,简称 BorrowerA:
0xcbfdffd7a2819a47fcd07dfa8bcb8a5deacc9ea8
稳定币 mkUSD:
0x4591dbff62656e7859afe5e45f6f47d3669fbb28
质押物 wstETH:
0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0
BorrowerOperations 合约:
0x72c590349535ad52e6953744cb2a36b409542719
攻击者观测 borrower 借贷情况,寻找抵押率较高的 borrower,在这比攻击交易中,找到的 borrower 为 BorrowerA。
攻击者获取 BorrowerA 在 TroveManager 中的 collateralToken(wstETH)和 debtToken(mkUSD)的数量,分别为824,599,953,913,164,625,273、598,174,188,906,400,741,697,930,在攻击者发起攻击时, wstETH 的价格大概为 $4155,大概估计一下抵押率为 570%,远远超过了最小抵押率 MCR 110%。这给后续攻击创造了条件。
攻击实施阶段主要是攻击者通过 mkUSD 的闪电贷服务,调用到漏洞合约MigrateTroveZap 的onFlashLoan函数,onFlashLoan函数可以对攻击者指定的 borrower 的 trove 进行置换,所谓置换即是先关闭这个 trove,再给 borrower 开启一个新的 trove。这个功能本来是用来将抵押物迁移到不同的 trove manager。然而这个功能存在漏洞,一是它没有校验新开的 trove 是否和之前的 trove 具有同样数量的抵押物,二是通过 mkUSD 的闪电贷服务,攻击者可以操控别人的 trove。具体的攻击步骤如下:
1、利用 mkUSD 的闪电贷服务调用到漏洞合约 MigrateTroveZap 的onFlashLoan函数,并在此时传入上述观测好的受害者 BorrowerA 的地址以及准备新开的 trove 的抵押品数量。攻击者调用 mkUSD 的flashLoan函数进行闪电贷,借出 mkUSD 给 MigrateTroveZap(MigrateTroveZap 合约用来自动将同样的抵押物迁移到不同的 trove manager),并在data参数中指定了后续操作需要用到的 BorrowerA 的地址、TroveManager 的地址、创建 trove 时抵押的wstETH 的数量,具体传入的参数如下所示:
receiver: MigrateTroveZaptoken: mkUSDamount: 598,174,188,906,400,741,697,930 (该数量正好是BorrowerA在TroveManager中借的mkUSD的数量)data(为了方便查看,以32字节划分): 000000000000000000000000cbfdffd7a2819a47fcd07dfa8bcb8a5deacc9ea80000000000000000000000001cc79f3f47bfc060b6f761fcd1afc6d399a968b60000000000000000000000001cc79f3f47bfc060b6f761fcd1afc6d399a968b60000000000000000000000000000000000000000000000000011c3794b4c52ff00000000000000000000000000000000000000000000000a6a488d5ae4bc84b6 //192125967324963177654000000000000000000000000e87c6f39881d5bf51cf46d3dc7e1c1731c2f790a00000000000000000000000089ee26fcdff6b109f81abc6876600ec427f7907f
mkUSD.flashLoan:
function flashLoan( IERC3156FlashBorrower receiver, address token, uint256 amount, bytes calldata data) external returns (bool) { require(token == address(this), "ERC20FlashMint: wrong token"); require(amount <= maxFlashLoan(token), "ERC20FlashMint: amount exceeds maxFlashLoan"); uint256 fee = _flashFee(amount); _mint(address(receiver), amount); require( receiver.onFlashLoan(msg.sender, token, amount, fee, data) == _RETURN_VALUE, "ERC20FlashMint: invalid return value" ); _spendAllowance(address(receiver), address(this), amount + fee); _burn(address(receiver), amount); _transfer(address(receiver), _prismaCore.feeReceiver(), fee); return true;}
在flashLoan函数中,首先会给 MigrateTroveZap 铸造598,174,188,906,400,741,697,930这么多 mkUSD,然后调用 MigrateTroveZap 的回调函数onFlashLoan,问题就出在了这个回调函数中。
2、在 MigrateTroveZap 的onFlashLoan函数中对 BorrowerA 的 trove 进行更换,之所以 MigrateTroveZap 能操作 BorrowerA 的 trove,是因为 BorrowerA 对 MigrateTroveZap 进行了委托授权。原本 BorrowerA 的 trove 抵押率比较高,但是抵押率比较高的 trove 被关闭掉,取而代之的是一个债务一样,但是抵押率更低的 trove,这个操作能够成功是因为更换 trove 时并没有校验新旧 trove 的抵押物数量是否一致,最终多余的抵押品数量会被留在 MigrateTroveZap 中。
详细过程如下:
在 MigrateTroveZap 的onFlashLoan函数中,会将指定 account 的抵押物从 troveManagerFrom 迁移到 troveManagerTo,在本次调用中,从攻击者传入的data参数中解析出来,这两个参数均指定为了TroveManager 的地址。
MigrateTroveZap.onFlashLoan:
function onFlashLoan( address, address, uint256 amount, uint256 fee, bytes calldata data) external returns (bytes32) { require(msg.sender == address(debtToken), "!DebtToken"); ( address account, address troveManagerFrom, address troveManagerTo, uint256 maxFeePercentage, uint256 coll, address upperHint, address lowerHint ) = abi.decode(data, (address, address, address, uint256, uint256, address, address)); uint256 toMint = amount + fee; borrowerOps.closeTrove(troveManagerFrom, account); borrowerOps.openTrove(troveManagerTo, account, maxFeePercentage, coll, toMint, upperHint, lowerHint); return _RETURN_VALUE;}
攻击者指定的 account 是 BorrowerA,因此首先会调用 BorrowerOperations 的closeTrove函数将 BorrowerA 的 trove 关闭掉,关闭之前会通过 modifier callerOrDelegated检查 BorrowerA 是否授权调用发起者(在这里是 MigrateTroveZap)代理它进行关闭 trove 的操作。由于 BorrowerA 确实授权了 MigrateTroveZap,因此判断通过,并且此时并不处于 recovery mode,因此 BorrowerOperations 最终调用TroveManager 的closeTrove函数关闭 trove,在这个过程中,将BorrowerA 的抵押品 wstETH(数量为824,599,953,913,164,625,273) 转给MigrateTroveZap。随后 burn 掉 MigrateTroveZap 的 mkUSD,数量为597,974,188,906,400,741,697,930。
BorrowerOperations.closeTrove:
function closeTrove(ITroveManager troveManager, address account) external callerOrDelegated(account) { IERC20 collateralToken;
uint256 price; bool isRecoveryMode; uint256 totalPricedCollateral; uint256 totalDebt; (collateralToken, price, totalPricedCollateral, totalDebt, isRecoveryMode) = _getCollateralAndTCRData( troveManager ); require(!isRecoveryMode, "BorrowerOps: Operation not permitted during Recovery Mode");
(uint256 coll, uint256 debt) = troveManager.applyPendingRewards(account);
uint256 newTCR = _getNewTCRFromTroveChange(totalPricedCollateral, totalDebt, coll * price, false, debt, false); _requireNewTCRisAboveCCR(newTCR);
troveManager.closeTrove(account, msg.sender, coll, debt);
emit TroveUpdated(account, 0, 0, 0, BorrowerOperation.closeTrove);
// Burn the repaid Debt from the user's balance and the gas compensation from the Gas Pool debtToken.burnWithGasCompensation(msg.sender, debt - DEBT_GAS_COMPENSATION);}
紧接着 BorrowerOperations 的openTrove函数被调用,此时传入的参数_collateralAmount为攻击者在data参数中指定的192,125,967,324,963,177,654,而_debtAmount为攻击者向 mkUSD 合约闪电贷的数量加上闪电贷的手续费,数值为598,712,545,676,416,502,365,458。经过计算,现在并不处于 recovery mode,因此最终用来计算抵押率的 debt 的数量compositeDebt为_debtAmount+ debt borrowing 费 + debt gas 补偿,具体数值为598,912,545,682,977,901,520,496,根据_collateralAmount、_debtAmount和价格计算出来的 ICR (Individual Collateral Rate) 约为 133%,大于 MCR (最小抵押率) 110%,符合创建 trove 的条件。随后 MigrateTroveZap 给TroveManager 转移_collateralAmount数量的 wstETH,MigrateTroveZap 拿到给其铸造的_debtAmount数量的 mkUSD。这番操作相当于攻击者利用 MigrateTroveZap 合约,将 BorrowerA 本来有很多抵押品的 trove,换成了抵押品较少的 trove,而攻击者想要盗取的,正是这一部分抵押品差额。
BorrowerOperations.openTrove:
function openTrove( ITroveManager troveManager, address account, uint256 _maxFeePercentage, uint256 _collateralAmount, uint256 _debtAmount, address _upperHint, address _lowerHint) external callerOrDelegated(account) { require(!PRISMA_CORE.paused(), "Deposits are paused"); IERC20 collateralToken; LocalVariables_openTrove memory vars; bool isRecoveryMode; ( collateralToken, vars.price, vars.totalPricedCollateral, vars.totalDebt, isRecoveryMode ) = _getCollateralAndTCRData(troveManager);
_requireValidMaxFeePercentage(_maxFeePercentage);
vars.netDebt = _debtAmount;
if (!isRecoveryMode) { vars.netDebt = vars.netDebt + _triggerBorrowingFee(troveManager, account, _maxFeePercentage, _debtAmount); } _requireAtLeastMinNetDebt(vars.netDebt);
// ICR is based on the composite debt, i.e. the requested Debt amount + Debt borrowing fee + Debt gas comp. vars.compositeDebt = _getCompositeDebt(vars.netDebt); vars.ICR = PrismaMath._computeCR(_collateralAmount, vars.compositeDebt, vars.price); vars.NICR = PrismaMath._computeNominalCR(_collateralAmount, vars.compositeDebt);
if (isRecoveryMode) { _requireICRisAboveCCR(vars.ICR); } else { _requireICRisAboveMCR(vars.ICR, troveManager.MCR()); uint256 newTCR = _getNewTCRFromTroveChange( vars.totalPricedCollateral, vars.totalDebt, _collateralAmount * vars.price, true, vars.compositeDebt, true ); // bools: coll increase, debt increase _requireNewTCRisAboveCCR(newTCR); }
// Create the trove (vars.stake, vars.arrayIndex) = troveManager.openTrove( account, _collateralAmount, vars.compositeDebt, vars.NICR, _upperHint, _lowerHint, isRecoveryMode ); emit TroveCreated(account, vars.arrayIndex);
// Move the collateral to the Trove Manager collateralToken.safeTransferFrom(msg.sender, address(troveManager), _collateralAmount);
// and mint the DebtAmount to the caller and gas compensation for Gas Pool debtToken.mintWithGasCompensation(msg.sender, _debtAmount);
emit TroveUpdated(account, vars.compositeDebt, _collateralAmount, vars.stake, BorrowerOperation.openTrove);}
3、回调结束以后,回到 mkUSD 的 flashLoan 函数。此时MigrateTroveZap 持有的 mkUSD 的数量正好比需要偿还的 mkUSD 的数量多一些,通过 burn 掉 MigrateTroveZap 的 mkUSD 成功偿还了闪电贷的债务。至此,MigrateTroveZap 相当于额外获得了约 632 个 wstETH。接下来攻击者就开始想办法把这 632 个 wstETH 给套到自己手上了。
收割赃款的步骤主要是攻击者将#攻击实施#步骤中盗取的留在MigrateTroveZap 合约中的wstETH 提取出来。攻击者依然是利用MigrateTroveZap 合约的漏洞。这一次攻击者先自己开了一个抵押率较低的 trove,然后通过 mkUSD 的闪电贷服务进入到漏洞合约 MigrateTroveZap 的onFlashLoan函数,对自己的 trove 进行更换。
相较于 #攻击实施# 阶段对 BorrowerA 的 trove 的更换,不同之处在于,攻击者对自己 trove 的更换,是要从低抵押率的 trove 更换为高抵押率的 trove,而用到的抵押物正是 MigrateTroveZap 中多出来的 wstETH。之所以能用到 MigrateTroveZap 的 wstETH,是因为攻击者也授权 MigrateTroveZap 对其 trove 进行管理,那么在更换 trove 时,抵押物都是在 TroveManager 和 MigrateTroveZap 之间流转。所以在开一个新的 trove 时,如果用到的抵押物数量增多,就会直接扣除 MigrateTroveZap 中的抵押品数量。
最终攻击者的 trove 在更换之后拥有了更多的抵押物,攻击者就可以自己发起关闭 trove 的调用,将抵押物提取出来,从而将盗取的抵押物收入囊中。
详细过程如下:
troveManager: 0x1cc79f3f47bfc060b6f761fcd1afc6d399a968b6 account: 0xd996073019c74b2fb94ead236e32032405bc027c _maxFeePercentage: 5,000,000,325,833,471_collateralAmount: 1,000,000,000,000,000,000_debtAmount: 2,000,000,000,000,000,000,000 _upperHint: 0xe87c6f39881d5bf51cf46d3dc7e1c1731c2f790a_lowerHint: 0x89ee26fcdff6b109f81abc6876600ec427f7907f
为了方便查看,以32字节划分:000000000000000000000000d996073019c74b2fb94ead236e32032405bc027c0000000000000000000000001cc79f3f47bfc060b6f761fcd1afc6d399a968b60000000000000000000000001cc79f3f47bfc060b6f761fcd1afc6d399a968b60000000000000000000000000000000000000000000000000011c3794b4c52ff0000000000000000000000000000000000000000000000225737b283504ff0c3 //633473986588201447619000000000000000000000000e87c6f39881d5bf51cf46d3dc7e1c1731c2f790a00000000000000000000000089ee26fcdff6b109f81abc6876600ec427f7907f
至此,收割赃款结束,攻击者获利约 632 个 wstETH。
本次攻击共涉及到三个 EOA,如下:
Exploiter 2 和 Exploiter 3 共获利约 200 个 ETH,而另外的约 3200 个 ETH 则是被声称是白帽的 Exploiter 1 获取 (https://etherscan.io/tx/0xc2825fd6dd05e8ec9f271d63efdebd06e78296afc0813c65788790567916d209)。目前 Prisma Finance 项目方仍在与声称是白帽的 Exploiter 1 沟通资金退还事宜。
Exploiter 1 向项目方开出了以下条件:
通过 ZAN 的 KYT 服务,我们可以看到,Exploiter 1 将资金转到了三个不同的地址上,最终这些资金一部分流入了 Tornado Cash 中。详细资金流转情况请查看链接:https://zan.top/kyt/controller/transaction?entity=0x7e39e3b3ff7adef2613d5cc49558eab74b9a4202&ecosystem=ethereum
转入 Tornado Cash 资金流动情况
Exploiter 2 的资金流转情况:https://zan.top/kyt/controller/transaction?entity=0x7fe83f45e0f53651b3ed9650d2a2c67d8855e385&ecosystem=ethereum
Exploiter 3 的资金流转情况:https://zan.top/kyt/controller/transaction/?entity=0x7C9FC6E2B908e858F30c5c71a20273315Efd5cf8&ecosystem=ethereum
通过分析本次攻击事件,我们有如下建议:
来源:ZAN Team
发布人:暖色
声明:该文观点仅代表作者本人,不代表火讯财经立场。火讯财经系信息发布平台,仅提供信息存储空间服务。
如文章涉及侵权, 请及时致函告之,本站将第⼀时间删除⽂章。邮箱:840034348@qq.com