重入漏洞:如何識別、利用和防止

在智能合約的世界裡,reentrancy 被視為最危險的漏洞之一。本文將幫助你不僅理解什麼是 reentrancy 攻擊,還有有效防範的方法。從基本技術到高級解決方案,我們將探索保護整個專案的各種方式。

Reentrancy 如何運作:基本攻擊機制

要理解 reentrancy,首先需要掌握一個基本概念:一個智能合約可以調用另一個合約,而在此過程中,第二個合約可以反向調用第一個合約,且此時第一個合約仍在執行中。

想像你有兩個合約:ContractA 持有 10 Ether,ContractB 已經向其中存入 1 Ether。當 ContractB 呼叫提款函數時,會先檢查餘額是否足夠。如果有,則 Ether 會被送回 ContractB。此時,如果沒有適當的保護措施,這正是攻擊者可以利用的弱點。

在典型的 reentrancy 攻擊中,攻擊者需要兩個函數:attack() 用來啟動攻擊,以及 fallback() 用來反覆調用。fallback 是 Solidity 中的一個特殊函數——沒有名稱、沒有參數,當 Ether 被送到合約且沒有附帶資料時,會自動調用。

Reentrancy 攻擊的逐步實現

讓我們跟蹤攻擊的每個步驟。攻擊者從自己的合約中調用 attack()。在此函數內,它會調用 ContractA 的 withdraw()。

當 ContractA 收到這個調用時,它會檢查 ContractB 是否有餘額大於 0。由於有 1 Ether,檢查通過。接著,ContractA 會將 1 Ether 發送回 ContractB,觸發 ContractB 的 fallback 函數。此時,ContractA 仍有 9 Ether,但更重要的是,ContractA 在帳本中的 ContractB 餘額仍未更新為 0。

這就是問題所在:fallback 會再次調用 ContractA 的 withdraw()。ContractA 再次檢查 ContractB 的餘額——仍是 1 Ether!為什麼?因為 balance[msg.sender] = 0 這行指令從未執行,因為它在送出 Ether 之後。

這個流程會不斷重複:調用 withdraw → 檢查餘額(仍 > 0)→ 送出 Ether → 觸發 fallback → 再次調用 withdraw……直到 ContractA 的所有 Ether 被提取完畢。

代碼分析:reentrancy 如何成為現實

EtherStore 合約是一個典型的易受攻擊範例。它有 deposit() 用來存款,和 withdrawAll() 用來提款。問題出在 withdrawAll() 的實作:它先檢查條件,然後送出 Ether,最後才更新餘額。

攻擊合約會利用這個漏洞。在建構函數中,攻擊者傳入 EtherStore 的地址,之後可以調用它的函數。攻擊合約的 fallback 會在 EtherStore 發送 Ether 時被調用,並在其中不斷調用 withdrawAll(),只要還有 Ether。attack() 函數則透過首次存入 1 Ether 來突破初始檢查。

結果是,EtherStore 的所有資金在一次交易中被全部提取。

三種防禦合約 reentrancy 的策略

為了保護智能合約,我們可以採用三個層級的防禦措施,從基本到全面。

noReentrant 模式:基本保護方案

最簡單的方法是使用 modifier noReentrant()。modifier 是 Solidity 中的一種特殊函數,可以用來修改其他函數的行為,而不需重寫整個函數。

基本想法是:當一個函數被 noReentrant() 保護時,它會在執行期間鎖定合約。任何試圖再次進入此函數的調用都會失敗,因為鎖定狀態阻止重入。只有當函數執行完畢並解鎖後,其他調用才會成功。

這個方案對單一功能的保護非常有效,但不適用於更複雜的情況。

Check-Effect-Interaction 模式:多功能防禦

第二個技術更強大:採用 Check-Effect-Interaction 模式。這個模式改變了函數的寫法。

核心原則是:先檢查條件(Check),再更新狀態(Effect),最後與外部合約交互(Interaction)。這樣即使攻擊者反覆調用,因為餘額已經在前面更新為 0,重入也不會成功。

例如,不要在送出 Ether 後才更新餘額,而是提前更新。這樣無論 fallback 如何調用,餘額都已經是 0。

此方法能在多個提款功能中提供持續的保護。

GlobalReentrancyGuard:全域性全面防禦

對於較大型、涉及多個合約交互的專案,我們需要更全面的方案:GlobalReentrancyGuard。

此方案不在單一函數層級鎖定,而是在整個專案層級鎖定。你可以建立一個專用的合約來存放全域鎖定狀態,所有其他合約都參照此合約。

想像這樣的場景:攻擊者調用 ScheduledTransfer 合約中的某個函數。經過檢查後,它會向 AttackTransfer 合約發送 Ether。AttackTransfer 的 fallback 被觸發,試圖再次調用 ScheduledTransfer,但由於全域鎖已啟用,調用被阻擋。

這個方法特別適合大型專案,避免不同合約間的重入問題。

選擇適合你專案的技術方案

選擇哪個策略取決於專案的複雜度。如果合約交互較少,noReentrant() 已足夠。如果有多個提款功能,Check-Effect-Interaction 模式是較佳選擇。若是大型、多合約的專案,GlobalReentrancyGuard 提供最全面的保護。

不論選擇哪一種,最重要的是理解 reentrancy 的運作方式,才能主動辨識並防範。

想要每日掌握智能合約安全資訊、源碼分析與最新 Web3 趨勢,請關注專業的 Solidity 安全資源。

查看原文
此頁面可能包含第三方內容,僅供參考(非陳述或保證),不應被視為 Gate 認可其觀點表述,也不得被視為財務或專業建議。詳見聲明
  • 打賞
  • 留言
  • 轉發
  • 分享
留言
請輸入留言內容
請輸入留言內容
暫無留言