[solidity hack] Reentrancy 重入攻擊
說明
假使有一智能合約名為 CryptoCurrencyStore,該合約實作了存款與取款功能,而使用者 A 和使用者 B 存入了兩枚以太幣,單位為 wei。
該智能合約的存款與取款 function 如下圖:
這時攻擊者發現了 CryptoCurrencyStore 的合約漏洞,並自行部署了一個智能合約,名為 AttackCryptoCurrencyStore,要來將 CryptoCurrencyStore 合約內的以太幣全數轉移到攻擊者的合約。
攻擊者在部署 AttackCryptoCurrencyStore 時會將 CryptoCurrencyStore 的合約地址傳入建構式,以此方便呼叫 CryptoCurrencyStore 內的 function。
一旦執行 attack function,就能將 CryptoCurrencyStore 內的以太幣全數清零並轉移到 AttackCryptoCurrencyStore。
其攻擊流程為:
- 攻擊者呼叫 attack function
- 執行 CryptoCurrencyStore 的 deposit function 進行存款,再呼叫 withdraw function 進行提款。
- 當 withdraw function 執行到 (bool sent, ) = msg.sender.call{value: userBalance}("") 這段時,將會觸發 AttackCryptoCurrencyStore 的 fallback。
- fallback function 內又會再呼叫 withdraw function,所以又會從 withdraw function 的第一段開始執行。
- 因過程中不會觸發到 require(sent, "Failed to send Ether"),導致攻擊者的以太幣並不會被 balances[msg.sender] = 0 初始化。
- 透過不斷地惡意循環,最終 CryptoCurrencyStore 內的以太幣將全數轉移至 AttackCryptoCurrencyStore。
nonReentrant
要有效抵擋重入攻擊,可以利用 modifier 設定變數 state 來讓攻擊者的 fallback 無法重複執行。
當攻擊者執行 withdraw function 時,因為有 nonReentrant modifier 的關係,第一次執行 lockedState 會被更改為 true,而當觸發到攻擊者的 fallback 時,會再一次的執行 withdraw,但因為這時 lockedState 為 true,使攻擊者並無法通過 require(!lockedState, "Block re-entrancy"),最終該筆交易將被 revert。
Updated balance before sent ether
另一種防範方式是先扣除使用者的存款餘額,再將錢轉至使用者的帳號地址內。
Slither
使用 Slither 測試原本無防護機制的程式碼:
設定防護機制後: