原文标题:《慢雾:Opyn 合约被黑详细分析》 2020 年 8 月 5 日,Opyn 合约遭遇黑客攻击。慢雾安全团队在收到情报后对本次攻击事件进行了全面的分析,下面为大家就这次攻击事件展开具体的技术分析。 攻击细节 逻辑分析 看其中 一笔攻击交易 通过查看内联交易可以看到攻击者仅使用 272ETH 最终得到 467ETH 使用 OKO 合约浏览器 对具体的攻击细节进行分析 关键点在于 oToken 合约的exercise函数,从上图中可以看出在exercise函数中通过调用两次 transfer 将 USDC 发送给攻击者合约,接下来我们切入exercise函数进行具体的分析 function exercise( uint256 oTokensToExercise, address payable[] memory vaultsToExerciseFrom ) public payable { for (uint256 i = 0; i < vaultsToExerciseFrom.length; i++) { address payable vaultOwner = vaultsToExerciseFrom[i]; require( hasVault(vaultOwner), "Cannot exercise from a vault that doesn't exist" ); Vault storage vault = vaults[vaultOwner]; if (oTokensToExercise == 0) { return; } else if (vault.oTokensIssued >= oTokensToExercise) { _exercise(oTokensToExercise, vaultOwner); return; } else { oTokensToExercise = oTokensToExercise.sub(vault.oTokensIssued); _exercise(vault.oTokensIssued, vaultOwner); } } require( oTokensToExercise == 0, "Specified vaults have insufficient collateral" ); } 可以看到exercise函数允许传入多个 vaultsToExerciseFrom,然后通过 for 循环调用_exercise函数对各个 vaultsToExerciseFrom 进行处理,现在我们切入_exercise函数进行具体的分析 function_exercise( uint256 oTokensToExercise, address payable vaultToExerciseFrom ) internal { // 1. before exercise window: revert require( isExerciseWindow(), "Can't exercise outside of the exercise window" ); require(hasVault(vaultToExerciseFrom), "Vault does not exist"); Vault storage vault = vaults[vaultToExerciseFrom]; require(oTokensToExercise > 0, "Can't exercise 0 oTokens"); // Check correct amount of oTokens passed in) require( oTokensToExercise <= vault.oTokensIssued, "Can't exercise more oTokens than the owner has" ); // Ensure person calling has enough oTokens require( balanceOf(msg.sender) >= oTokensToExercise, "Not enough oTokens" ); // 1. Check sufficient underlying // 1.1 update underlying balances uint256 amtUnderlyingToPay = underlyingRequiredToExercise( oTokensToExercise ); vault.underlying = vault.underlying.add(amtUnderlyingToPay); // 2. Calculate Collateral to pay // 2.1 Payout enough collateral to get (strikePrice * oTokens) amount of collateral uint256 amtCollateralToPay = calculateCollateralToPay( oTokensToExercise, Number(1, 0) ); // 2.2 Take a small fee on every exercise uint256 amtFee = calculateCollateralToPay( oTokensToExercise, transactionFee ); totalFee = totalFee.add(amtFee); uint256 totalCollateralToPay = amtCollateralToPay.add(amtFee); require( totalCollateralToPay <= vault.collateral, "Vault underwater, can't exercise" ); // 3. Update collateral + oToken balances vault.collateral = vault.collateral.sub(totalCollateralToPay); vault.oTokensIssued = vault.oTokensIssued.sub(oTokensToExercise); // 4. Transfer in underlying, burn oTokens + pay out collateral // 4.1 Transfer in underlying if (isETH(underlying)) { require(msg.value == amtUnderlyingToPay, "Incorrect msg.value"); } else { require( underlying.transferFrom( msg.sender, address(this), amtUnderlyingToPay ), "Could not transfer in tokens" ); } // 4.2 burn oTokens _burn(msg.sender, oTokensToExercise); // 4.3 Pay out collateral transferCollateral(msg.sender, amtCollateralToPay); emit Exercise( amtUnderlyingToPay, amtCollateralToPay, msg.sender, vaultToExerciseFrom ); } 在代码第 6 行首先检查了现在是否在保险期限内,这自然是肯定的在代码第 11 行则对 vaultToExerciseFrom 是否创建了 vault 进行检查,注意这里只是检查了是否有创建 vault在代码第 14、16、21 行对传入的 oTokensToExercise 值进行了检查,在上图 OKO 浏览器中我们可以看到攻击者传入了 0x1443fd000,这显然是可以通过检查的接下来在代码第 28 行计算需要消耗的 ETH 数量在代码第 35、41 行计算需要支付的数量与手续费接下来在代码第 59 行对 underlying 是否是 ETH 地址进行判断,而 underlying 在上面代码第 31 行进行了赋值,由于 isETH 为 true, 因此将会进入 if 逻辑而不会走 else 逻辑,在 if 逻辑中 amtUnderlyingToPay 与 msg.value 都是用户可控的随后对 oTokensToExercise 进行了燃烧,并调用 transferCollateral 函数将 USDC 转给exercise函数的调用者以上关键的地方在于步骤 2 与步骤 6,因此我们只需要确保传入的 vaultToExerciseFrom 都创建了 vault,且使 amtUnderlyingToPay 与 msg.value 相等即可,而这些相关参数都是我们可以控制的,所以攻击思路就显而易见了。 思路验证 让我们通过攻击者的操作来验证此过程是否如我们所想: 1、首先在保险期限内是肯定的 2、攻击者传入的 vaultToExerciseFrom 分别为: 0xe7870231992ab4b1a01814fa0a599115fe94203f0x076c95c6cd2eb823acc6347fdf5b3dd9b83511e4 经验证,这两个地址都创建了 vault 3、攻击者调用exercise传入 oTokensToExercise 为 0x1443fd000 (5440000000),msg.value 为 272ETH,vaultsToExerciseFrom 分别为以上两个地址 4、此时由于此前攻击者创建的 oToken 为 0xa21fe800 (2720000000),及 vault.oTokensIssued 为 2720000000 小于 5440000000,所以将走exercise函数中的 else 逻辑,此时 oTokensToExercise 为 0xa21fe800 (2720000000),则以上代码第 60 行 msg.value == amtUnderlyingToPay 是肯定成立的 5、由于 vaultsToExerciseFrom 传入两个地址,所以 for 循环将执行两次_exercise函数,因此将 transfer 两次把 USDC 转给攻击者合约 完整的攻击流程如下 攻击者使用合约先调用 Opyn 合约的 createERC20CollateralOption 函数创建 oToken攻击合约调用exercise函数,传入已创建 vault 的地址通过exercise函数中 for 循环逻辑执行调用两次_exercise函数4.exercise函数调用 transferCollateral 函数将 USDC 转给函数调用者 (由于 for 循环调用两次 _exercise 函数,transferCollateral 函数也将执行两次)攻击合约调用 removeUnderlying 函数将此前传入的 ETH 转出最终攻击者拿回了此前投入的 ETH 以及额外的 USDC 攻击合约地址 0xe7870231992Ab4b1A01814FA0A599115FE94203f Opyn 合约地址 0x951D51bAeFb72319d9FBE941E1615938d89ABfe2 攻击交易 (其一) 0xa858463f30a08c6f3410ed456e59277fbe62ff14225754d2bb0b4f6a75fdc8ad 修复建议 此次攻击主要是利用了_exercise函数中对 vaultToExerciseFrom 是否创建 vault 的检查缺陷。此检查未校验 vaultToExerciseFrom 是否是调用者自己,而只是简单的检查是否创建了 vault,导致攻击者可以任意传入已创建 vault 的地址来通过检查。 建议如下: 在处理用户可控的参数时应做好权限判断,限制 vaultToExerciseFrom 需为调用者本人。项目方可以在项目初期或未完成多次严谨安全审计之前添加合约暂停功能与可升级模型,避免在发生黑天鹅事件时无法有效的保证剩余资金安全。来源链接:mp.weixin.qq.com —- 编译者/作者:慢雾科技 玩币族申明:玩币族作为开放的资讯翻译/分享平台,所提供的所有资讯仅代表作者个人观点,与玩币族平台立场无关,且不构成任何投资理财建议。文章版权归原作者所有。 |
慢雾:技术详解 Opyn 智能合约被黑损失 37 万美元过程
2020-08-05 慢雾科技 来源:链闻
LOADING...
相关阅读:
- Harmony携手Chainlink 百万基金赋能Chainlink集成应用2020-08-05
- 可口可乐加入DeFi World!2020-08-05
- EOS的新骗局网站2020-08-05
- 天生说: 8.5 ETH下午行情分析2020-08-05
- 新的二层技术zkPorter:结合zkRollup理念和分片2020-08-05