跳到正文

重入攻擊

重入攻擊利用外部呼叫或回呼,在合約狀態不一致時再次進入合約邏輯。本文說明攻擊路徑、防護方法與審查邊界。

更新於

僅供教育用途,不構成投資建議。重入漏洞可能迅速造成不可逆的合約資產損失,任何單一防護措施或審計都不能證明合約絕對安全。

直接回答

當合約邏輯發起外部呼叫,而被呼叫方在原始執行尚未結束、合約不變量尚未恢復之前回呼該合約時,就可能發生重入攻擊。如果過時的餘額、份額、權限或價格仍然可見,回呼就可能利用不一致的狀態重複某項操作或影響另一項操作。

回呼不必進入同一個函式,也不一定涉及原生貨幣轉帳。代幣鉤子、NFT 接收方回呼、金庫或策略呼叫以及閃電貸回呼都可能把控制權交給不受信任的程式碼。重入可以跨函式、跨合約和跨模組發生;唯讀重入也能讓另一個協議使用暫時值。

運作方式

典型利用過程如下:

  1. 攻擊者進入一個會改變狀態的函式並通過初始檢查。
  2. 易受攻擊的合約在完成記帳前呼叫某個地址、代幣或協議。
  3. 攻擊者控制的程式碼在舊狀態仍可見時進入原合約或相連合約。
  4. 巢狀呼叫重複某項效果或改變共享狀態,隨後執行逐層返回,但不變量已經被破壞。

外部控制權轉移可能很明顯,例如底層呼叫;也可能隱藏在代幣轉帳、接收方鉤子、安全鑄造、策略配接器或任意呼叫介面之後。回滾通常會撤銷發生回滾的呼叫樹,但攻擊者可以安排一組成功的巢狀呼叫,在提取價值或破壞記帳後正常返回。

防護應當分層:

  • 遵循檢查-生效-互動模式:先驗證,再提交所有相關內部狀態變化,最後才進行外部互動。
  • 對共享受保護不變量的所有入口使用重入鎖,而不只保護含有明顯呼叫的函式。
  • 在合適場景採用主動領取模式,並儘量減少對不受信任的代幣、接收方、鉤子、代理和整合的呼叫。
  • 定義跨合約不變量,並測試惡意回呼、跨函式路徑、巢狀多重呼叫和唯讀使用方。
  • 在設計允許時,將實作審查與監控、暫停和事件回應控制結合起來。

範例

假設某金庫發送價值,並且僅在呼叫之後才清空使用者餘額:

function withdraw() external {
    uint256 amount = balances[msg.sender];
    require(amount > 0, "empty balance");

    (bool ok, ) = msg.sender.call{value: amount}("");
    require(ok, "transfer failed");

    balances[msg.sender] = 0;
}

balances[msg.sender] 仍保留舊數額時,收款方已取得控制權。其接收函式可以再次呼叫 withdraw(),通過同一項檢查並請求再次轉帳。把餘額更新移到呼叫之前可消除這一特定窗口;重入鎖可以拒絕巢狀進入。但兩者都不能證明整個系統安全,因為其他入口或相連合約仍可能暴露同一項未完成的不變量。

風險

  • 重入鎖只保護一個函式,而另一個函式暴露了相同狀態。
  • 局部呼叫順序正確,但跨合約不變量在回呼期間仍不一致。
  • 代幣、接收方、閃電貸回呼或策略配接器意外執行外部程式碼。
  • 檢視函式發布暫時價格或兌換率,另一個協議在同一筆交易中使用了它。
  • 升級、模組或儲存布局變更繞過或破壞原有鎖。
  • 測試涵蓋遞迴提款,卻遺漏跨函式、跨合約和唯讀路徑。

對使用者而言,審計標識或重入鎖只是存在某項控制措施的證據,不是安全保證。升級、整合和特權緊急操作都可能改變攻擊面。應限制授權與風險曝險,核實已部署的實作,並且不要假定損失能夠撤銷。

常見誤解

  • 重入只是重複呼叫同一個提款函式。 它也可能進入另一個函式或合約,或者向讀取方暴露不一致的資料。
  • 只有原生貨幣轉帳才會觸發回呼。 代幣標準、接收方鉤子和協議整合同樣可以執行外部程式碼。
  • 重入鎖或檢查-生效-互動能保證合約安全。 其範圍、每個共享入口和跨合約不變量仍需審查。
  • 審計通過就排除了重入風險。 審計只在有限範圍內進行;後續變更和未經審查的整合可能引入新路徑。

相關主題

來源

導覽

搜尋知識庫...