原標題:《慢霧:Opyn合約被黑詳細分析》

By : Kong @慢霧安全團隊

背景

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、項目方可以在項目初期或未完成多次嚴謹安全審計之前添加合約暫停功能與可升級模型,避免在發生黑天鵝事件時無法有效的保證剩餘資金安全。