技术详解Opyn智能合约遭受攻击损失37万美元过程 – 作者:SlowMist慢雾科技

背景

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

攻击细节

逻辑分析

看其中一笔攻击交易:

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

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

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

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

3319b74113cde8abf56510090719b390_GEYTINBKG42TC.png

关键点在于 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、首先在保险期限内是肯定的

9142de6b33570346df97f5fc28ec5fd0_GI4DKKRRGM2A.png

2、攻击者传入的 vaultToExerciseFrom 分别为:

0xe7870231992ab4b1a01814fa0a599115fe94203f

0x076c95c6cd2eb823acc6347fdf5b3dd9b83511e4

图片[4]-技术详解Opyn智能合约遭受攻击损失37万美元过程 – 作者:SlowMist慢雾科技-安全小百科

经验证,这两个地址都创建了 vault

3、攻击者调用 exercise传入 oTokensToExercise 为 0x1443fd000 (5440000000),msg.value 为 272ETH,vaultsToExerciseFrom 分别为以上两个地址

4e948c51f5fae24735ebdf542c105f2b_GEYDGNJKGIYDO.png

4、此时由于此前攻击者创建的 oToken 为 0xa21fe800 (2720000000),及 vault.oTokensIssued 为 2720000000 小于 5440000000,所以将走 exercise函数中的 else 逻辑,此时 oTokensToExercise 为 0xa21fe800 (2720000000),则以上代码第 60 行 msg.value == amtUnderlyingToPay 是肯定成立的

ef81fdd6a2055282cc13c863cf8647c4_GQ2TMKRRGYZQ.png

5、由于 vaultsToExerciseFrom 传入两个地址,所以 for 循环将执行两次 _exercise 函数,因此将 transfer 两次把 USDC 转给攻击者合约

7faf5742a142715b83b306bd168fef83_HE4TIKRWGM3Q.png

完整的攻击流程如下

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

来源:freebuf.com 2020-08-06 14:47:52 by: SlowMist慢雾科技

© 版权声明
THE END
喜欢就支持一下吧
点赞0
分享
评论 抢沙发

请登录后发表评论