header-langage
简体中文
繁體中文
English
Tiếng Việt
한국어
日本語
ภาษาไทย
Türkçe
掃碼下載APP
Partnered With:

深入探討重入攻擊如何盜走Curve池7000萬美元

閱讀本文需 17 分鐘
有關重入攻擊的技術科普。
原文標題:《A Deep Dive Into How Curve Pool』s $ 70 Million Reentrancy Exploit Was Possible》
原文作者:CyberPunkMetalHead
原文翻譯:Odaily 星球日報


近期的 Curve 池漏洞與我們在過去幾年裡看到的大多數加密貨幣黑客事件有所不同,因為與之前的許多漏洞不同,這一次並不直接與智能合約本身的漏洞有關,而是與它所使用的語言的底層編譯器有關。


在這裡,我們談論的是 Vyper:一個面向智能合約的、具有 Pythonic 風格的編程語言,旨在與以太坊虛擬機(EVM)交互。我對此次漏洞的背後原因非常感興趣,所以我決定深入研究。


隨著這次漏洞的發展,每天的新聞頭條都在報告新的數字。現在看來,情況終於得到了控制,但在此之前已經有超過 7000 萬美元被盜。根據 LlamaRisk 的事後評估,截止到今天,有幾個 DeFi 項目的池子也被黑客攻破,包括 PEGD 的 pETH/ETH: 1100 萬美元;Metronome 的  msETH/ETH: 340 萬美元;Alchemix 的 alETH/ETH: 2260 萬美元;和 Curve DAO: 大約 2470 萬美元。


這次漏洞被稱為重入錯誤,它是在 Vyper 編程語言的某些版本上出現的,特別是 v  0.2.15、v 0.2.16 和 v 0.3.0 。因此,使用這些特定版本的 Vyper 的所有項目都可能成為攻擊的目標。


什麼是重入(reentrancy)?  


為了理解這次漏洞為什麼會發生,我們首先需要了解什麼是重入以及它是如何工作的。


如果一個函數在執行過程中可以被中斷,並且在其之前的調用完成執行之前可以安全地再次被調用(「重新進入」),則稱該函數為可重入的。可重入函數在硬件中斷處理、遞歸等應用中都有使用。


為了使一個函數變得可重入,它需要滿足以下條件:


- 它不能使用全局和靜態數據。這只是一種約定,沒有硬性的限制,但如果使用全局數據的函數被中斷和重新啟動,它可能會丟失信息。


- 它不應修改自己的代碼。無論函數何時被中斷,都應該能夠以相同的方式執行。這可以管理,但通常不建議這樣做。


- 它不應該調用其他非重入函數。重入不應與線程安全混淆,儘管它們緊密相關。一個函數可以是線程安全的,但仍然不是可重入的。為了避免混淆,重入只涉及到一個線程的執行。這是在沒有多任務操作系統存在的時代的一個概念。


這裡有一個實際的例子:


i = 5 
< blockquote>def non_reentrant_function():
 return i** 5 
def reentrant_function(number:int):
 return number ** 5 


函數non_reentrant_function:


- 這個函數沒有參數。


- 它直接返回全局變量 i 的五次方。


- 所以當你調用這個函數時,它總是返回 5** 5 ,即 3125 。


函數reentrant_function:


- 這個函數有一個參數 number,是整型。


- 它返回參數 number 的五次方。


- 這意味著你可以給這個函數傳入任何整數,並得到這個數的五次方作為返回值。例如,如果你傳入 2 ,它會返回 2 的 5 次方,即 32 。


值得注意的是,許多智能合約函數都不是可重入的,因為它們訪問如錢包餘額之類的全局信息。


什麼是鎖(Lock)?


鎖本質上是一種線程同步機制,某個進程可以聲稱或「鎖定」另一個進程。


最簡單的鎖類型被稱為二進制信號量。這種鎖為被鎖定的數據提供獨占訪問。還有更複雜的鎖類型,可以提供對讀數據的共享訪問。在編程中誤用鎖可能導致死鎖或活鎖,進程持續互相阻塞,狀態不斷改變但沒有進展。


編程語言在後台使用鎖來優雅地管理和共享多個子程序之間的狀態更改。但是,某些語言,如 C# 和 Vyper 允許在代碼中直接使用鎖。


@nonreentrant('lock')
def func():
 assert not self.locked , "locked"
 self.locked = True
 # Do stuff
 # Release the lock after finishing doing stuff
 raw_call(msg.sender, b"", value= 0)
 self.locked = False
 # More code here< /blockquote>


在上面的例子中,我們希望確保如果 msg.sender(合同呼叫者)是另一個合同,它不會在執行時調用代碼。如果在 raw_call() 下面還有更多的代碼,而沒有鎖,msg.sender 可能會在我們的函數執行完畢之前調用上面的所有代碼。


因此,在 Vyper 中,nonreentrant(『lock』) 裝飾器是一種控制對函數的訪問的機制,以防止調用者在它們完成運行之前反复執行智能合約函數。


在許多 DeFi 黑客事件中,通常都是合約開發者沒有預見到的智能合約錯誤,一個聰明但惡意的利用者發現了某些函數或數據暴露的方式中的弱點。但這次的情況獨特之處在於,Curve 的智能合約以及所有其他成為攻擊受害者的池和項目在代碼本身中都沒有已知的漏洞。合同是穩固的。


nonreentrant(『lock』) 是存在的。


由於 Vyper 語言在處理重入鎖的方式上出現了問題,導致了這個問題的發生。所以,合約創建者可能部署了看似合理的代碼,但由於編譯器沒有正確處理鎖,使得攻擊者能夠利用這個有缺陷的鎖進行利用,導致合約行為出現意料之外的結果。


讓我們看看真正受到重入攻擊的合約。注意 @nonreentrant(『lock』) 修飾符嗎?通常情況下,這應該可以防止重入,但實際上並未能防止。攻擊者能夠在函數返回結果之前反複調用 remove_liquidity()。


@nonreentrant('lock')
def remove_liquidity(
   _burn_amount: uint  ;256,
   _min_amounts: uint 256 [N_COINS],
   _receiver: address = msg.sender
) -> uint 256 [N_COINS]:
   """
   @notice Withdraw coins from the pool
   @dev Withdrawal amounts are based on current deposit ratios
   @param _burn_amount Quantity of LP tokens to burn in the withdrawal< /blockquote>
   @param _min_amounts Minimum amounts of underlying coins to receive
   @param _receiver Address that receives the withdrawn coins
   @return List of amounts of coins that were withdrawn
   """
   total_supply: uint  256 = self.totalSupply
   amounts: uint 256 [N_COINS] = empty(uint 256 [N_COINS])
    ;for i in range(N_COINS):
       old_balance: uint 256 = self.balances[i]
  ;      value: uint 256 = old_balance * _burn_amount / total_supply
       assert value >= _min_amounts[i], "Withdrawal resulted in fewer coins than expected"
       self.balances[i] = old_balance - value
< blockquote>       amounts[i] = value
       if i == 0:< /blockquote>
           raw_call(_receiver, b"", value=value)
   ;     else:
           response: Bytes[ 32  ] = raw_call(
               self.coins[ 1 ] ,
               concat(
   ;                 method_id("transfer(address, uint 256)") ,
                   convert(_receiver, bytes 32),
                    convert(value, bytes 32),
               ),
               max_outsize= 32,
            )
            if len(response) > 0:
               assert convert(response , bool)
   total_supply -= _burn_amount
   self.balanceOf[msg.sender] -= _burn_amount
   self.totalSupply = total_supply
   log Transfer(msg.sender, ZERO_ADDRESS, _burn_amount)
   log RemoveLiquidity(msg.sender, amounts, empty(uint 256 [N_COINS]), total_supply)
   return amounts


這是如何被利用的?


到目前為止,我們知道重入攻擊是一種反複調用智能合約中的某個函數的方法。但這是如何導致資金被盜和在 Curve 攻擊中損失 7000 萬美元的呢?


注意智能合約末尾的 self.balanceOf[msg.sender] -= _burn_amount 嗎?這告訴智能合約池中 msg.sender 的流動性,減去燃燒費。接下來的代碼行為 message.sender 調用 transfer()。


因此,一個惡意合約可以在金額更新之前不斷地調用提現,幾乎讓他們可以選擇提取池中的所有流動性。


這樣的攻擊通常的流程是這樣的:


- 易受攻擊的合約有 10 個 eth。


- 攻擊者調用存款並存入 1 個 eth。


- 攻擊者調用提現 1 個 eth,此時提現函數執行一些檢查:


< /p>

- 攻擊者的賬戶中是否有 1 個 eth?是的。


- 將 1 個 eth 轉移到惡意合約。注意:合約的餘額尚未更改,因為該函數仍在執行。


- 攻擊者再次調用提現 1 個 eth。 (重新入場)


- 攻擊者的賬戶中是否有 1 個 eth?是的。


這將重複,直到池中沒有更多的流動性。


Vyper 語言中的這個問題已經被修復,在 0.3.0 版本之後不再存在。如果您是開發人員,或使用 Vyper 的 Web3 組織,請確保立即更新您的版本。


來源鏈接


歡迎加入律動 BlockBeats 官方社群:

Telegram 訂閱群:https://t.me/theblockbeats

Telegram 交流群:https://t.me/BlockBeats_App

Twitter 官方帳號:https://twitter.com/BlockBeatsAsia

举报 糾錯/舉報
選擇文庫
新增文庫
取消
完成
新增文庫
僅自己可見
公開
保存
糾錯/舉報
提交