风险提示:防范以"虚拟货币""区块链"名义进行非法集资的风险 —银保监会等五部门

一文了解Opyn 合约被黑详细分析

背景

2020 年 8 月 5 日,Opyn 合约遭遇黑客攻击。慢雾安全团队在收到情报后对本次攻击事件进行了全面的分析,下面为大家就这次攻击事件展开具体的技术分析。

攻击细节

逻辑分析

看其中一笔攻击交易:

https://etherscan.io/tx/0xa858463f30a08c6f3410ed456e59277fbe62ff14225754d2bb0b4f6a75fdc8ad

慢雾科技

通过查看内联交易可以看到攻击者仅使用 272ETH 最终得到 467ETH

使用 OKO 合约浏览器对具体的攻击细节进行分析

https://oko.palkeo.com/0xa858463f30a08c6f3410ed456e59277fbe62ff14225754d2bb0b4f6a75fdc8ad/

慢雾科技

关键点在于 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 );
    }

    1、在代码第 6 行首先检查了现在是否在保险期限内,这自然是肯定的

    2、在代码第 11 行则对 vaultToExerciseFrom 是否创建了 vault 进行检查,注意这里只是检查了是否有创建 vault

    3、在代码第 14、16、21 行对传入的 oTokensToExercise 值进行了检查,在上图 OKO 浏览器中我们可以看到攻击者传入了 0x1443fd000,这显然是可以通过检查的

    4、接下来在代码第 28 行计算需要消耗的 ETH 数量

    5、在代码第 35、41 行计算需要支付的数量与手续费

    6、接下来在代码第 59 行对 underlying 是否是 ETH 地址进行判断,而 underlying 在上面代码第 31 行进行了赋值,由于 isETH 为 true, 因此将会进入 if 逻辑而不会走 else 逻辑,在 if 逻辑中 amtUnderlyingToPay 与 msg.value 都是用户可控的

    7、随后对 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 转给攻击者合约

    慢雾科技

    完整的攻击流程如下

    1、攻击者使用合约先调用 Opyn 合约的 createERC20CollateralOption 函数创建 oToken

    2、攻击合约调用 exercise 函数,传入已创建 vault 的地址

    3、通过 exercise 函数中 for 循环逻辑执行调用两次 _exercise 函数

    4、exercise 函数调用 transferCollateral 函数将 USDC 转给函数调用者(由于 for 循环调用两次 _exercise 函数,transferCollateral 函数也将执行两次)

    5、攻击合约调用 removeUnderlying 函数将此前传入的 ETH 转出

    6、最终攻击者拿回了此前投入的 ETH 以及额外的 USDC

    攻击合约地址

    0xe7870231992Ab4b1A01814FA0A599115FE94203f

    Opyn 合约地址

    0x951D51bAeFb72319d9FBE941E1615938d89ABfe2

    攻击交易(其一)

    0xa858463f30a08c6f3410ed456e59277fbe62ff14225754d2bb0b4f6a75fdc8ad

    修复建议

    此次攻击主要是利用了 _exercise 函数中对 vaultToExerciseFrom 是否创建 vault 的检查缺陷。此检查未校验 vaultToExerciseFrom 是否是调用者自己,而只是简单的检查是否创建了 vault,导致攻击者可以任意传入已创建 vault 的地址来通过检查。

    建议如下:

    1、在处理用户可控的参数时应做好权限判断,限制 vaultToExerciseFrom 需为调用者本人。

    2、项目方可以在项目初期或未完成多次严谨安全审计之前添加合约暂停功能与可升级模型,避免在发生黑天鹅事件时无法有效的保证剩余资金安全。

    如有疑问联系邮箱:352064354@qq.com
    星际传媒版权及免责声明:仅作为区块链信息平台,本站所提供的资讯信息不代表任何投资暗示,本站所发布文章仅代表个人观点,与星际传媒官方立场无关。
    *本文转载自网络转载,版权归原作者所有。本站只是转载分享,不代表赞同其中观点。请自行判断风险,本文不构成投资建议。*