重入攻擊的技術原理是什麼,為什麼合約程式碼的「順序」那麼重要?重入攻擊的根本漏洞,在於一個智能合約在把錢打出去之後,才去更新自己的帳本——這個操作順序讓攻擊有機可乘。正確的操作應該是:先更新帳本(把用戶的餘額設為 0),再把錢打給用戶。有漏洞的寫法是:先把錢打給用戶,等打款完成後再更新帳本。問題在於:以太坊的 .call() 方法打款時,如果接收方是一個智能合約,它可以在接收到 ETH 的那一刻觸發一個「fallback 函數(回調函數)」。攻擊者在這個回調函數裡再次呼叫原合約的「withdraw」——此時原合約的帳本還沒更新,所以餘額檢查仍然通過。這個「重新進入(Re-enter)」的操作可以反覆執行,直到合約裡的 ETH 被抽完。整個漏洞的邏輯,就是「先打錢後記帳」這個錯誤順序。
The DAO 攻擊是什麼,它為什麼那麼重要?The DAO(Decentralized Autonomous Organization)是 2016 年的一個以太坊上的眾籌智能合約項目,募集了當時價值約 1.5 億美元的 ETH。在 2016 年 6 月,一名攻擊者利用重入漏洞,在 The DAO 的提款合約中反覆提款,最終轉走了約 360 萬枚 ETH(當時價值約 6,000 萬美元)。這個事件的影響遠超一般駭客攻擊:以太坊社群面臨了一個根本性的抉擇——要不要硬分叉(修改區塊鏈狀態)來把被盜的 ETH 強行歸還給 DAO 投資人?最終,以太坊社群以多數決的方式決定硬分叉,把以太坊「回滾」到攻擊發生前的狀態,歸還被盜資金。這就是今天以太坊(ETH)的由來;而拒絕承認這次硬分叉的少數人繼續維護原鏈,這就是以太坊經典(ETC)的由來。The DAO 攻擊讓整個加密行業深刻認識到智能合約安全的重要性,也奠定了「區塊鏈不可篡改性」和「治理緊急響應」之間存在根本矛盾的討論基礎。
如何防禦重入攻擊?有哪些已被驗證的模式?幾個標準的防禦方式。第一,Checks-Effects-Interactions(CEI)模式:這是最基本也最重要的防禦原則。合約的任何函數應按這個順序執行:Checks(先做所有的條件檢查)→ Effects(再更新合約的所有內部狀態)→ Interactions(最後才和外部合約或地址互動,例如打款)。把狀態更新放到打款之前,攻擊者即使重入,餘額也已經是 0 了,檢查不通過,攻擊失效。第二,ReentrancyGuard(重入鎖):OpenZeppelin 提供了一個標準的 ReentrancyGuard 模組,本質上是一個布林值的互斥鎖——函數執行時設為「已鎖」,完成後解鎖;若在「已鎖」狀態下有人再次呼叫,直接回滾。這是一個簡單有效的防護,適合複雜的函數調用鏈。第三,避免使用原始 call() 打款:transfer() 和 send() 方法預設只轉發 2300 Gas,不夠一個有複雜邏輯的回調函數執行,可以從結構上限制重入的可行性(但這不應作為主要防禦,因為 Gas 機制可能隨升級改變)。
除了重入攻擊,還有哪些常見的智能合約漏洞類型?了解重入攻擊之後,還有幾種同樣重要的智能合約漏洞類型。整數溢位/下溢(Integer Overflow/Underflow):在引入 Solidity 0.8 自動溢位保護之前,數值計算可以「繞一圈」(例如 uint8 的 255 + 1 = 0)——被利用來偽造餘額。現代 Solidity 已有保護,但舊合約仍有風險。不安全的外部呼叫(Unsafe External Call):呼叫外部合約時,若不正確處理回傳值,可能讓攻擊者控制執行流程。邏輯錯誤(Logic Error):不是技術漏洞,而是業務邏輯的設計錯誤——條件判斷顛倒、許可權控制缺失等,讓攻擊者可以不按預期的方式使用合約。閃電貸攻擊(Flash Loan Attack):利用閃電貸的瞬間巨大流動性,在一筆交易內操控市場或預言機價格,影響依賴這些價格的借貸或清算邏輯。了解這些漏洞,是評估 DeFi 協議安全性的基礎知識。
用偽程式碼說明重入攻擊的完整流程。想像一個簡單的以太坊存取款合約,內部有一個 balances 映射記錄每個地址的餘額。有漏洞的 withdraw 函數:
function withdraw() external {
require(balances[msg.sender] > 0); // 檢查餘額
(bool success,) = msg.sender.call{value: balances[msg.sender]}(""); // 先打款
require(success);
balances[msg.sender] = 0; // 後更新餘額
}
攻擊者部署了一個惡意合約,裡面有一個 fallback 函數:
fallback() external payable {
if (address(victim).balance > 1 ether) {
victim.withdraw(); // 在收到 ETH 的當下立刻再次呼叫
}
}
攻擊過程:攻擊者呼叫 victim.withdraw();合約檢查餘額 > 0(通過);合約打款給攻擊者;攻擊者的 fallback 被觸發,立刻再次呼叫 victim.withdraw();此時合約的 balances[攻擊者] 仍然是原始值(因為更新在打款之後);檢查再次通過,再次打款;……循環,直到合約沒錢。
修復版的正確順序(CEI):先 balances[msg.sender] = 0,再 .call{value: ...}。一行順序的改變,就阻斷了整個攻擊。
重入攻擊揭示的,是智能合約設計一個根本性的工程取捨:外部呼叫(把資金打給另一個地址或合約)和狀態更新之間的先後順序。「先打錢、後記帳」的直覺式寫法,讓業務邏輯更清晰(先給你錢,再說你沒錢了),但引入了重入風險;「先記帳、後打錢」(CEI)雖然在業務邏輯上稍顯彆扭,但完全消除了重入問題。這個取捨現在已有明確的共識答案——永遠用 CEI——但它提醒我們,智能合約程式碼的安全性和直覺的業務邏輯之間有時會有微妙的衝突。每一個智能合約都是公開的、不可更改的——漏洞一旦部署,修補只能靠新合約的遷移,而無法像傳統軟體那樣靜默推送修補。這讓智能合約開發在上線前的審計,比任何傳統軟體都更為關鍵。