首先,我將以簡單的方式幫助您理解什麼是重入攻擊以及如何防止它,然後,我將深入研究代碼示例以展示漏洞在哪裏,攻擊者代碼是什麼,最重要的是,我將向您展示最新的經過驗證的方法,不僅可以保護項目中的一個智能合約,而且可以保護項目中的所有智能合約。

劇透:如果您已經聽說過 nonReentrant() 修飾符,請繼續閱讀,因爲您將發現 globalNonReentrant() 修飾符和 tests-effects-interactions 模式下面的幾行。



在上圖中,我們有 ContractA 和 ContractB。現在,如你所知,智能合約可以與另一個智能合約交互,例如在本例中,ContractA 可以調用 ContractB。因此,重入性的最基本思想是 ContractB 能夠在 ContractA 仍在執行時回調 ContractA。

那麼,攻擊者如何利用這一點呢?

上面我們有 ContractA,它有 10 個 Ether,我們看到 ContractB 在 ContractA 中存儲了 1 個 Ether。在這種情況下,ContractB 將能夠使用 ContractA 中的 withdraw 函數,並在通過檢查(其餘額大於 0)後將 Ether 發送回自身,然後將其總餘額修改爲 0。



現在讓我們看看 ContractB 如何利用重入漏洞利用 withdraw 函數並從 ContractA 竊取所有 Ether。基本上,攻擊者需要兩個函數:attack() 和 fallback()。

在 Solidity 中,fallback 函數是一個沒有名稱、參數或返回值的外部函數。任何人都可以通過以下方式調用 fallback 函數:調用合約中不存在的函數;調用函數而不傳入所需數據;向合約發送不包含任何數據的 Ether。

重入的工作方式(讓我們一步一步地沿着箭頭走)是攻擊者調用 attack() 函數,該函數內部調用 ContractA 的 withdraw() 函數。在函數內部,它將驗證 ContractB 的餘額是否大於 0,如果是,則繼續執行。



由於 ContractB 的餘額大於 0,它會將 1 個 Ether 發回,並觸發 fallback 函數。請注意,此時 ContractA 有 9 個 Ether,而 ContractB 已經有 1 個 Ether。



接下來,當 fallback 函數執行時,它會再次觸發 ContractA 的 withdraw 函數,再次檢查 ContractB 的餘額是否大於 0。如果你再次檢查上面的圖片,你會注意到它的餘額仍然是 1 個以太幣。



這意味着檢查通過,它會向 ContractB 發送另一個 Ether,從而觸發 fallback 函數。請注意,由於“balance=0”這一行從未執行過,因此這種情況會一直持續到 ContractA 中的所有 Ether 都用完爲止。

___________

現在讓我們看一下可以使用 Solidity 代碼識別重入的智能合約。



在 EtherStore 合約中,我們有函數 deposit() 來存儲和更新發送者的餘額,然後有 withdrawAll() 函數來一次性獲取所有存儲的餘額。請注意 withdrawAll() 的實現,它首先檢查 require 餘額是否大於 0,然後立即發送 Ether,同樣,最後將發送者的餘額更新爲 0。



這裏我們有一個合約 Attack,它將使用重入來耗盡 EtherStore 合約。讓我們分析一下它的代碼:

  • 在其構造函數中,攻擊者將傳遞 EtherStore 地址以創建實例並能夠使用其功能。

  • 我們可以看到,當 EtherStore 向該合約發送 Ether 時,fallback() 函數將被調用。只要餘額大於或等於 1,它就會調用從 EtherStore 提取資金。

  • 在 attack() 函數中,我們有利用 EtherStore 的邏輯。如我們所見,首先我們將通過確保我們有足夠的以太幣來發起攻擊,然後存入 1 以太幣以使 EtherStore 中的餘額大於 0,從而在開始提款之前通過檢查。

我在上文的 ContractA 和 ContractB 示例中一步步解釋了代碼的運行方式,現在我們來總結一下。首先,攻擊者會調用 attack(),而 attack() 內部會從 EtherStore 調用 withdrawAll(),然後 withdrawAll() 會將 Ether 發送到 Attack 合約的 fallback 函數。然後攻擊者會啓動重入並耗盡 EtherStore 的餘額。

那麼,我們如何保護我們的合約免受重入攻擊呢?

我將向您展示三種預防技術,以全面保護它們。我將介紹如何防止單個函數中的重入、跨函數的重入以及跨合約的重入。



保護單個函數的第一種方法是使用名爲 noReentrant 的修飾符。

修飾符是一種特殊類型的函數,可用於修改其他函數的行爲。修飾符允許您向函數添加額外的條件或功能,而無需重寫整個函數。

我們在這裏所做的是在執行函數時鎖定合約。這樣它將無法重新進入單個函數,因爲它需要遍歷函數的代碼,然後將鎖定狀態變量更改爲 false,以便再次通過在 require 中完成的檢查。

___________



第二種技術是利用 Checks-Effects-Interactions 模式來保護我們的合約免遭跨功能重入攻擊。你能發現上面更新後的 EtherStore 合約中發生了什麼變化嗎?

爲了深入瞭解 Check-Effects-Interaction 模式,我建議閱讀 https://fravoll.github.io/solidity-patterns/checks_effects_interactions.html





上面我們看到了左圖中存在漏洞的代碼的比較,其中餘額在發送以太幣後更新,如上所示,這可能永遠無法達到,而右邊的代碼所做的就是將 balances[msg.sender] = 0(或效果)移到 require(bal > 0)(檢查)之後但在發送以太幣(交互)之前。

這樣,我們可以確保即使另一個函數正在訪問 drawAll(),該合約也將受到保護,免受攻擊者攻擊,因爲在發送以太幣之前餘額將始終更新。

圖案由 https://twitter.com/GMX_IO 創建

我要向您展示的第三種技術是創建 GlobalReentrancyGuard 合約來防止跨合約重入。重要的是要理解,這適用於具有多個相互交互的合約的項目。

這裏的想法與我在第一種技術中解釋過的 noReentrant 修飾符相同,它進入修飾符,更新一個變量以鎖定合約,直到代碼完成纔會解鎖。這裏最大的區別是,我們使用存儲在單獨合約中的變量,該變量用作檢查函數是否已進入的地方。







我在這裏創建了一個示例,沒有實際的代碼,只有函數名稱作爲參考來理解這個想法,因爲根據我的經驗,它可以幫助形象化情況,而不僅僅是用文字寫出來。

在這裏,攻擊者將調用 ScheduledTransfer 合約中的函數,該函數在滿足條件後會將指定的 Ether 發送到 AttackTransfer 合約,因此 AttackTransfer 合約將進入 fallback 函數,從而從 ScheduledTransfer 合約的角度“取消”交易,但仍會收到 Ether。這樣,它就會開始觀察,直到耗盡 ScheduledTransfer 中的所有 Ether。

好吧,使用我上面提到的 GlobalReentrancyGuard 就可以避免這種攻擊場景。

__________________

Twitter @TheBlockChainer 可以找到更多關於智能合約、Web3 安全、Solidity、審計智能合約等的每日更新。

__________________