重入攻擊,在The DAO 被黑的事件中被使用過,主要是開發者編寫的Solidity 代碼的一些漏洞造成的。
在這篇文章中,我們會在以太坊早期的一次漏洞中通過Solidity 代碼黑客的攻擊。這次事件黑客,攻擊了一個坊間,最有名地進行了一次中性的攻擊。 道 的DAO(去中心化組織),這次攻擊事件中用的方法通常被稱為重攻擊。
知識分類
理解這個攻擊前你需要了解以下內容:
- 區塊鏈技術的基礎知識,特別是以太坊。
- 以太坊虛擬機(EVM):在以太坊節點上運行化的,相互同步的機器狀態器。
- 在以太坊語境下,智能合約指的是通過Solidity 語言編寫的軟件代碼,在EVM 上執行。
- 在以太坊語境下,“賬本”這個詞是一個有以太餘額的主體,可以在以太坊網絡上發送交易,有類型:用戶控制和部署的智能合約。
你不需要關於Solidity 實例的任何知識,因為代碼的任何知識都很簡單。對於編程語言的任何基礎知識都可以幫助理解。
你也可以隨著這裡的重入攻擊教程一起編程。
關於The DAO 被攻擊事件的簡介
在2015 年之前,還在早期的以太坊社區就開始討論 DAO(去中心化自動化組織)DAO 確實是在以太坊區塊鏈協議上運行的智能合約協議,可以通過社區代碼之間的智能協議運行,同時通過社區代碼的協作來進行去往中心化的決策。在2016 年,也是以太坊主網運行了一年以後,一個名為“The DAO”的DAO 被創建了。它是一個去中心化的,由社區控制的投資基金。它通過銷售自己的這些社區籌集了價值1億美元的以太幣(大概有354萬ETH)。人們通過存儲ETH來購買DAO的社區通證,存儲在DAO中的ETH就變成了The DAO會代表社區通證的投資者來進行投資。
正因為如此,以太坊、合約當時、DAO發展的很早,所以這些合約的智能組織和非人類協調活動的令人高興。
然而,在The DAO 開始還流行的一個時間裡,就被“黑帽”黑客在接下來的一周裡,這個黑客從The DAO 的智能合約中偷走了價值1 5000 萬美元的ETH。這個黑客的攻擊成為“重入”攻擊。 “重入”攻擊。名字以太坊也非常描述了我的方法,在深入了解測試。對你的投資者的一樣,這次DAO 的揭幕,使人們失去了信心,同時影響了的信譽。
內部的參與者和評論員都看到了在DAO 中偷走的部分人認為處理了資金,並就如何處理這個過程進行了整個過程,無法進行的討論。如果強密碼學保證了區塊鏈的篡改,修改,即使是為了為了的去信仰和防篡改系統也應該是竄改去。化,防篡改這些特點所需要的典型。
而偷盜,有人覺得在暗藏資產正在慢慢地偷走,這會破壞公眾的自信心。
在這些進行的時候,一個討論“白帽”黑客組織進行反擊。他們想要進入陣營的營壘,他們使用黑客走的同樣的手段進行攻擊,嘗試比黑客地把DAO 的資金轉給他們。大量的資金被退回給了投資者,這樣很多投資者就能夠通過這個“逃生艙”取回他們的投資。
由於黑客利用黑客竊取的資金盜走的行為還在繼續,以太坊核心團隊面臨著一個嚴峻的決定。分叉以太坊就可以修改歷史,讓發生過的事。分叉操作把錢廢除,黑客將不再對以太坊發起攻擊。此時將不再行使權力,不再有效地行使權力,從而有效地破壞了這些非法行為的攻擊行為。的原則:這種烤製以太坊本身就想要的那種中心化的,單方面的行為。
那些投票的人也有分派給不同的街頭坊區塊鏈,然後這8個分叉的人就開始接受了總投票的5%,礦工挖掘做法這個,因為這家以太坊合約忽略了任何問題,這是人的)。也是為什麼現在有兩個以太坊鏈—— 以太經典和我們今天在用的以太坊。他們擁有ETH 通證,當時這些通證在市場上的價格不同。你可以在這裡查看以太坊基金會在的聲明。
The DAO 在歷史上的重要意義,基於The DAO 黑客事件和最終決策同樣影響了歷史。但是非常的黑客如何攻擊?讓我們一起了解一下。
Solidity 中的重入攻擊是什麼?
合約是一些,最常見的語言命名為Solidity,它們在任何區塊鏈上被稱為“智能合約”。和其他客戶的合約在整個交易平台上的可以和其他客戶的智能合約中使用。特性都是通過以太坊虛擬機執行Solidity 來的。
重入攻擊通過一個命名為“fallback”的函數執行。後備函數是Solidity 這些特殊的結構,在某些特殊的場景下會被觸發。 fallback 函數的功能如下:
- 不是修飾的。
- 是被外部調用的(不能夠被它們自己契約內部的函數調用)。
- 一個合約中只有0 個或1 個後備函數,不會更多。
- 會在另外一個合約中引用一個本合約中不存在的函數時被調用。
- 當ETH 被發送給該合約的時候,如果該交易沒有calldata 合約中沒有receive() 函數時,回退函數會被觸發。在這種情況下,回退被標記為應付,以有效可以被觸發並接受以太幣。
- Fallback 函數可以包含自己的邏輯。
它就是因為第五個和第六個特性,導致後備函數被重入。攻擊同時也依賴於被攻擊合約的代碼順序。讓我們一起了解以下是攻擊發生的。
我基於將被攻擊的DAO 的中性,和只是攻擊的盒子是版本,同時描述更有趣,我將在一個事件版本中,為了了解下重來重入攻擊,下面是的代碼和DAO 的實際代碼也不一樣。
在DAO 的餘額和合約中,The DAO 有一個狀態變量描述中的智能合約,用來記錄所有的DAO 餘額的投資。 。
部署黑客拿到了一份合約,發送了一份合約,“投資者”在The DAO 中存儲了一些ETH。然後黑客去調用DAO 合約中的withdraw() 函數。 ETH。但是黑客的合約中沒有它()函數,所以當黑客合約中的回退()函數就被觸發了。這個回退函數可以沒有邏輯,只接受ETH,但是契約黑客中的回退卻包含一些惡意代碼。
這些惡意代碼,在被執行的時候,再次調用DAO 的智能合約的withdraw() 函數。這會開始一個循環調用,因為這時候第一個調用仍然在執行。它只執行在黑客合約中的回退函數完成才能夠完成,withdraw()函數在以後的黑客契約中的回退函數中再次被調用,就這樣開始了一個黑客契約和DAO契約之間的循環執行。
每次被調用的時候,DAO都會給(但合約發送與它以後等值的ETH存儲)。關鍵是黑客的賬本餘額只有在發送ETH的時候才能完成。發送ETH的合約只有在黑客的後備函數執行完成後才能提現。所以DAO 的合約不斷空給合約的ETH,同時又不發送餘額,因此會導致DAO 的所有資金。
看看下面的代碼,可能會更好地理解一點。
重攻擊入代碼樣例
讓我們開始看DAO 的代碼,代碼中的一個執行順序導致了這個漏洞。
// SPDX-License-Identifier: MIT pragma solidity ^0.8.10; contract Dao { mapping(address => uint256) public balances; function deposit() public payable { require(msg.value >= 1 ether, "Deposits must be no less than 1 Ether"); balances[msg.sender] += msg.value; } function withdraw() public { // Check user's balance require( balances[msg.sender] >= 1 ether, "Insufficient funds. Cannot withdraw" ); uint256 bal = balances[msg.sender]; // Withdraw user's balance (bool sent, ) = msg.sender.call{value: bal}(""); require(sent, "Failed to withdraw sender's balance"); // Update user's balance. balances[msg.sender] = 0; } function daoBalance() public view returns (uint256) { return address(this).balance; } }
下面的注意點:
- 智能合約有一個投資者地址和這個ETH餘額的映射。投資的ETH都被記錄在合約的餘額中,這個於合約的狀態變量餘額不一樣。
- deposit() 函數要求的金額是1 ETH,當投資金額收到以後,最少會增加投資者的餘額。
- withdraw()函數在把餘額變成0之前,只有被獲取的ETH發給投資者(使用msg.sender.call)。發送ETH的交易結束在黑客的回退函數完成之後才可以執行,所以黑客的可以只有餘額在回退函數結束之後才會被置0。這就是The DAO契約的最大漏洞。
現在讓我們看看發動了重入攻擊的智能合約。
// SPDX-License-Identifier: MIT pragma solidity ^0.8.10; interface IDao { function withdraw() external ; function deposit()external payable; } contract Hacker{ IDao dao; constructor(address _dao){ dao = IDao(_dao); } function attack() public payable { // Seed the Dao with at least 1 Ether. require(msg.value >= 1 ether, "Need at least 1 ether to commence attack."); dao.deposit{value: msg.value}(); // Withdraw from Dao. dao.withdraw(); } fallback() external payable{ if(address(dao).balance >= 1 ether){ dao.withdraw(); } } function getBalance()public view returns (uint){ return address(this).balance; } }
下面的注意點:
- attack() 函數黑客將的“投資”存到了DAO 中,然後通過調用DAO 合約的withdraw() 函數開始攻擊,就像我們之前所說的一樣。
- Fallback 函數包含了因為惡意代碼,所以它會檢查DAO 合約中是否還有ETH 剩餘,調用DAO 合約的withdraw() 函數。在之前看到ETH 的交易還沒有發送完成,所以DAO 的withdraw()函數並沒有賬戶餘額。這個一直在被執行是因為黑客的後備函數經常調用withdraw()。這樣變量就不會改變餘額這個狀態的情況下,提空DAO合約中所有的餘額。
- The DAO 合約的ETH 餘額提現,回退()函數就不會再被執行撤消()函數了,然後回退()函數的最多完成。只有這個時候,黑客的賬戶餘額會置0, The DAO 也沒有任何ETH 了。
修復重入攻擊漏洞
有幾種方法去修復重入實例的漏洞,但是在我們的中,最簡單的修復方法是改變DAO 合約中的withdraw() 函數的執行順序,以讓調用者的餘額在DAO 發送給他們ETH之前置0。就像下面的代碼一樣:
Contract Dao { … function withdraw() public { // Check user's balance require( balances[msg.sender] >= 1 ether, "Insufficient funds. Cannot withdraw" ); uint256 bal = balances[msg.sender]; // Update user's balance. balances[msg.sender] = 0; // Withdraw user's balance (bool sent, ) = msg.sender.call{value: bal}(""); require(sent, "Failed to withdraw sender's balance");// Update user's balance.balances[msg.sender] = 0;} }
通過這個,當更動的調用()函數觸發黑客的回退()函數以後,這個函數嘗試重入退出()函數時,黑客的重入餘額的時候就已經是0了,require()函數會判斷為false,因此這裡revert這個交易。這最開始調用fallback的交易直接返回,因為交易失敗,所以發送的返回false,下一行的代碼(require(sent,“Failed to提取發件人的餘額”);) 恢復。
黑客只能取回他自己存的錢,但不會有更多的。
另一個方法是DAO 契約使用函數修改器,將退出()函數“鎖住”以下代碼,我們可以在被重入的時候被這個鎖擋住。
Contract Dao { bool internal locked; modifier noReentrancy() { require(!locked, "No reentrancy"); locked = true; _; locked = false; } //…… function withdraw() public noReentrancy { // withdraw logic goes here… } }
這個重入守護使用了互斥(互斥)標誌 當黑客契約的fallback()函數再次通過withdarw()嘗試函數進入DAO的時候,這個函數修改器的時候,它在上一次調用沒有完成的情況下被再次調用。並且被觸發,同時它需要函數會revert 返回信息“No reentrantancy”。
總結
這篇文章用了一個非常簡單的例子,解釋了來重入攻擊的概念。儘管我使用了The DAO 的背景去解釋重入攻擊,但是DAO 的代碼是不同的。然而,The DAO 通過“進入”這個概念被黑客攻擊在下的條件下,可以通過不重整的方式更新你的資產,獲取DAO 的GitHub。 回購 中見,並且在 提交歷史 中了解這個漏洞是如何被修復的。
您可以關注鏈家禽機資料和私信加入者社區,有大量關於智能合約開發的學習以及關於區塊鏈的話題!
帖子 重入攻擊和DAO 被黑事件 首先出現在 Chainlink 博客.