智能合約概述
智能合約是運行在區(qū)塊鏈網(wǎng)絡(luò)中的一段程序,經(jīng)由多方機構(gòu)自動執(zhí)行預(yù)先設(shè)定的邏輯,程序執(zhí)行后,網(wǎng)絡(luò)上的最終狀態(tài)將不可改變。智能合約本質(zhì)上是傳統(tǒng)合約的數(shù)字版本,由去中心化的計算機網(wǎng)絡(luò)執(zhí)行,而不是由政府或銀行等中央集權(quán)機構(gòu)執(zhí)行。智能合約程序可以用Solidity或Vyper等編程語言實現(xiàn),并存儲在區(qū)塊鏈上,在公鏈網(wǎng)絡(luò)上,任何人都可以訪問和執(zhí)行部署好的智能合約。
智能合約擁有防篡改、透明和自動化等特征,這使其非常適合于金融交易,供應(yīng)鏈管理等應(yīng)用場景,其次,在商業(yè)保險,游戲,環(huán)保等領(lǐng)域都有所應(yīng)用?,F(xiàn)如今,區(qū)塊鏈被視作為一種潛在的革命性技術(shù),可以改變許多行業(yè)的協(xié)議制定和執(zhí)行方式。
安全問題分析解決
智能合約既然是一段程序代碼,同樣會存在著缺陷或者錯誤導(dǎo)致出現(xiàn)致命的安全漏洞,在執(zhí)行過程中,存在諸多的風(fēng)險,并不能保證其完全安全。事實上,大多數(shù)的智能合約都和金融資產(chǎn)有所關(guān)聯(lián),其對應(yīng)的智能合約漏洞的利用,意味著用戶資產(chǎn)的損失,比如代幣失竊,執(zhí)行未經(jīng)授權(quán)的交易,甚至是拖垮整個區(qū)塊鏈網(wǎng)絡(luò)。在這篇文章中,我們將談?wù)撟畛R姷闹悄芎霞s安全問題,以及處理這些問題的方法。
不安全的算術(shù)運算(Insecure Arithmetic)
這是一類非常經(jīng)典的漏洞,主要來源于未經(jīng)檢查的算術(shù)運算。在Solidity 0.8.x以前,當(dāng)一個整數(shù)變量達到其范圍的下限或上限時,它將自動變?yōu)橐粋€較低或較高的數(shù)字。
漏洞描述
function batchTransfer(address[] _receivers, uint256 _value) public whenNotPaused returns (bool) {
uint cnt = _receivers.length;
// 計算應(yīng)付總金額
uint256 amount = uint256(cnt) * _value;
require(cnt > 0 && cnt <= 20);
require(_value > 0 && balances[msg.sender] >= amount);
balances[msg.sender] = balances[msg.sender].sub(amount);
for (uint i = 0; i < cnt; i++) {
balances[_receivers[i]] = balances[_receivers[i]].add(_value);
Transfer(msg.sender, _receivers[i], _value);
}
return true;
}
以上的智能合約函數(shù)實現(xiàn)了一個批量轉(zhuǎn)賬的功能,將合約賬戶上的資金分別轉(zhuǎn)給多個地址(不超過20個)。主要漏洞在以下這行代碼:
uint256 amount = uint256(cnt) * _value;
攻擊者可以傳入一個比較大的數(shù)值,使得計算出來的amount
值很小,小于了自己賬戶里的可用余額,從而通過了可用余額的校驗,最終得到了一大筆資金入賬。
解決方案
- 將Solidity編譯器升級至0.8.0及其以上的版本,會自動檢測數(shù)值溢出的異常;
- 如果不方便升級Solidity編譯器的話,可以考慮使用安全的三方庫(比如Open Zeppelin),實現(xiàn)安全可信的算術(shù)運算;
- 將以上的有漏洞的代碼改為:
function batchTransfer(address[] _receivers, uint256 _value) public whenNotPaused returns (bool) {
uint cnt = _receivers.length;
// 計算應(yīng)付總金額
uint256 amount = uint256(cnt) * _value;
require(cnt > 0 && cnt <= 20);
// 使用除法換算出來的值要等于傳入的_value
require(amout / uint256(cnt) == _value)
require(_value > 0 && balances[msg.sender] >= amount);
balances[msg.sender] = balances[msg.sender].sub(amount);
for (uint i = 0; i < cnt; i++) {
balances[_receivers[i]] = balances[_receivers[i]].add(_value);
Transfer(msg.sender, _receivers[i], _value);
}
return true;
}
越權(quán)攻擊(Exceed Authority Access Attack)
通常有兩種情況會導(dǎo)致越權(quán)攻擊:
- 不恰當(dāng)?shù)暮瘮?shù)可見性設(shè)置。如果不顯式指定函數(shù)可見性,那么默認為
public
,意味著允許未經(jīng)授權(quán)的用戶調(diào)用該函數(shù); - 沒有設(shè)置
owner
,某些關(guān)鍵性的函數(shù)不可被任意訪問,而是應(yīng)該指定特定的使用者。
漏洞描述
如以下代碼所示,由于_sendWinnings
函數(shù)沒有設(shè)置可見性,默認是 public
,攻擊者可以通過調(diào)用此函數(shù)直接竊取資金。
contract HashForEther {
function withdrawWinnings() {
// 錢包地址十六進制的后8位全是0
require(uint32(msg.sender) == 0);
_sendWinnings();
}
function _sendWinnings() {
msg.sender.transfer(this.balance);
}
}
解決方案
- 將
_sendWinnings
函數(shù)的可見性設(shè)置為private
- 給
_sendWinnings
函數(shù)限制調(diào)用者,通常是管理員或者合約部署者
contract HashForEther {
address private _owner;
constructor(address owner) {
_owner = owner;
}
modifier ownerable() {
require(_owner == msg.sender);
_;
}
function withdrawWinnings() public {
// 錢包地址十六進制的后8位全是0
require(uint32(msg.sender) == 0);
_sendWinnings();
}
function _sendWinnings() public ownerable {
msg.sender.transfer(this.balance);
}
}
重入攻擊(Reentrancy attack)
重入攻擊是存在以太坊上最常見的智能合約安全漏洞。在以太坊中,對其他智能合約函數(shù)的調(diào)用并非異步進行的,也就是意味著自身的智能合約繼續(xù)執(zhí)行之前,會等待外部方法的執(zhí)行結(jié)束,這將非常有可能導(dǎo)致被調(diào)用的合約的中間狀態(tài)被不合理的利用。
漏洞描述
pragma solidity 0.8.17;
contract EtherStore {
// 存儲鏈上地址與對應(yīng)的可用余額
mapping(address => uint) public balances;
function deposit() public payable {
// 消息調(diào)用者在該合約中的存款加上賬戶當(dāng)余額
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");
// 將地址對應(yīng)的修改為0
balances[msg.sender] = 0;
}
}
以上是一個簡單的存款/提款的智能合約,漏洞主要出現(xiàn)在以下的一行代碼:
(bool sent, ) = msg.sender.call{value: bal}("");
能使得以上漏洞被成功利用,是具備了三個條件:
- call函數(shù)的調(diào)用沒有交易手續(xù)費(Gas)限制,默認會使用所有剩余的Gas,這是用于執(zhí)行智能合約的以太坊虛擬機的特性;
-
msg.sender
是來自另外一個惡意智能合約的地址,當(dāng)收到交易轉(zhuǎn)賬后,會觸發(fā)fallback
函數(shù); - 發(fā)起攻擊的智能合約實現(xiàn)
fallback
函數(shù),主要是再一次觸發(fā)被攻擊的智能合約的提款函數(shù)。
實際上,兩個智能合約之間的調(diào)用已經(jīng)進入了“遞歸黑洞”,攻擊者只需要向被攻擊的智能合約中存入少量的資金,通過不斷調(diào)用提款函數(shù),可以提取超額的回報。
解決方案
-
使用send()或者transfer()函數(shù),因為有Gas限制,最多消耗2300Gwei;
-
慎用外部函數(shù),檢查每一個直接或者間接調(diào)用外部函數(shù)的地方,確保狀態(tài)變更完成之后,再調(diào)用;
function withdraw() external { uint bal = balances[msg.sender]; require(bal > 0); // 先更新余額變化,再發(fā)送資金 // 重入攻擊的時候,balances[msg.sender]已經(jīng)被更新為0了,不能通過上面的檢查。 balanceOf[msg.sender] = 0; (bool success, ) = msg.sender.call{value: bal}(""); require(success, "Failed to send Ether"); }
-
為每一個賬戶地址增加重入標識,操作執(zhí)行完成之前,不允許重復(fù)執(zhí)行相同的邏輯。
uint private _status; // 重入鎖 // 重入鎖 modifier nonReentrant() { // 在第一次調(diào)用 nonReentrant 時,_status 將是 0 require(_status == 0, "ReentrancyGuard: reentrant call"); // 在此之后對 nonReentrant 的任何調(diào)用都將失敗 _status = 1; _; // 調(diào)用結(jié)束,將 _status 恢復(fù)為0 _status = 0; } // 只需要用nonReentrant重入鎖修飾withdraw()函數(shù),就可以預(yù)防重入攻擊了。 function withdraw() external nonReentrant { uint bal = balances[msg.sender]; // 判斷是否有可用余額 require(bal > 0); // 提取全部的金額 (bool sent, ) = msg.sender.call{value: bal}(""); require(sent, "Failed to send Ether"); // 將地址對應(yīng)的修改為0 balances[msg.sender] = 0; }
拒絕服務(wù)攻擊(DoS Attack)
正常情況下,一個智能合約對外提供穩(wěn)定的服務(wù)是基于一個大前提:在耗盡交易手續(xù)費(Gas)之前,智能合約程序可以正常執(zhí)行結(jié)束。攻擊者正是破壞了這一個大前提,使得智能合約不能正常提供服務(wù)。
漏洞描述
以下是一個拍賣的智能合約,主要的功能是價高者勝出,未中標的買家將會被立即退還競拍保證金。
contract Auction {
address currentLeader;
uint highestBid;
constructor () {
currentLeader = msg.sender;
highestBid = 1;
}
function bid() payable {
require(msg.value > highestBid);
(bool success, ) = currentLeader.call{value: highestBid}("");
require(success, "Refund failed");
currentLeader = msg.sender;
highestBid = msg.value;
}
}
將會產(chǎn)生漏洞的代碼是:
(bool success, ) = currentLeader.call{value: highestBid}("");
攻擊者可以制造一個惡意的智能合約,實現(xiàn)了fallback回調(diào)函數(shù),在fallback函數(shù)內(nèi)回滾交易。這個智能合約持續(xù)向拍賣合約發(fā)起攻擊,一旦自己成為了最高價者,在試圖退還競拍保證金的時候,由于惡意智能合約的fallback函數(shù),返回的success的值是false,導(dǎo)致退還失敗,在這之后的賦值新的競拍者的代碼邏輯將永遠不會執(zhí)行到,其他競拍者也就沒有機會獲得成功。
解決方案
解決以上漏洞,最主要是分開競拍和退款兩個操作。若競拍失敗,先記錄退款地址,再單獨提供退款的操作,由用戶自行提取競拍保證金。
contract Auction {
address highestBidder;
uint highestBid;
mapping(address => uint) refunds;
constructor () {
currentLeader = msg.sender;
highestBid = 1;
}
function bid() payable external {
require(msg.value >= highestBid);
if (highestBidder != address(0)) {
// 記錄要退款的金額
refunds[highestBidder] += highestBid;
}
highestBidder = msg.sender;
highestBid = msg.value;
}
// 單獨提供退款操作
function withdrawRefund() external {
uint refund = refunds[msg.sender];
refunds[msg.sender] = 0;
(bool success, ) = msg.sender.call.value(refund)("");
require(success);
}
}
值得注意的是,這里不建議開啟一個循環(huán)自動處理退款,有兩個原因:
- 退款地址可能是一個惡意攻擊的合約地址;
- 退款地址數(shù)量很大,Gas耗費巨大,不能保證全部的退款能到賬。
蜜罐攻擊(Honeypot Attack)
一些智能合約會故意暴露顯而易見的“漏洞”,通常情況下,用戶會發(fā)送資金,以期獲得超額的回報,最終卻被該智能合約“反咬一口”,不但沒有獲得預(yù)期的回報,反而損失了本金。
漏洞描述
contract CryptoRoulette {
uint256 private secretNumber;
uint256 public lastPlayed;
uint256 public betPrice = 0.001 ether;
address public ownerAddr;
struct Game {
address player;
uint256 number;
}
Game[] public gamesPlayed;
constructor() public {
ownerAddr = msg.sender;
shuffle();
}
function shuffle() internal {
// 中獎號碼設(shè)置為一個固定的數(shù)字6
secretNumber = 6;
}
function play(uint256 number) payable public {
require(msg.value >= betPrice && number <= 10);
Game game;
game.player = msg.sender;
game.number = number;
gamesPlayed.push(game);
if (number == secretNumber) {
// 如果傳入的數(shù)字正好是中獎號碼,則可以贏取獎金
msg.sender.transfer(this.balance);
}
//shuffle();
lastPlayed = now;
}
function kill() public {
if (msg.sender == ownerAddr && now > lastPlayed + 6 hours) {
suicide(msg.sender);
}
}
function() public payable { }
}
如上述代碼所示,很容易被注意到,初始化的中獎號碼是6,但是實際調(diào)用play(6)
之后,并不會如期贏取獎金。其原因,主要是Game
變量未實例化,EVM的存儲機制決定了secretNumber
最終的值已不再是6了,而是智能合約的調(diào)用者的地址,所以參與者始終都不會得到獎金。
如上圖所示,EVM的存儲結(jié)構(gòu)是由 2^256 個插槽 (Slot)組成,每個插糟有 32byte,等同于256bit,正好是可以存放一個uint256
類型的變量,合約中的狀態(tài)變量會根據(jù)其具體類型分別順序保存到這些插槽中。
在play
函數(shù)中,因為Game
并沒有初始化,對game.player
和game.number
的賦值,實際上是分別對Slot0
和Slot1
進行了賦值,按照變量定義的順序,其分別是secretNumber
和lastPlayed
。如果用戶傳入的number
是6的話,與實際的secretNumber
的值是不相等的,非但不能獲得獎金,而且還損失了本金。
解決方案
從用戶視角來看,作為合約的調(diào)用方/使用者,需要甄別對方的智能合約的實現(xiàn)是否合理,除了使用未經(jīng)實例化的局部變量,還有諸如Solidity版本過低,使用了未知的代理合約,引用了惡意的代碼庫等等。
除此之外,應(yīng)該多關(guān)注業(yè)界發(fā)生的安全事件,及其相關(guān)的資訊文章,比如 mirror、DL News,也可以借助一些工具和平臺,輔助交易,比如 BlockSec,F(xiàn)lashbots。
智能合約升級
智能合約與傳統(tǒng)應(yīng)用程序有一個不同的地方在于智能合約一經(jīng)發(fā)布于區(qū)塊鏈上就無法篡改,即使智能合約中有漏洞需要修復(fù),或者需要對業(yè)務(wù)邏輯進行變更,它也不能在原有的合約上直接修改再重新發(fā)布,因此在設(shè)計之初就需要結(jié)合業(yè)務(wù)場景考慮合理的升級機制。
按照程序升級的通常意義來理解,升級后的程序首先是要滿足用戶的正常使用,用戶的信息和資產(chǎn)沒有丟失,其次是最好能做到兼容和適配以往的版本。
實現(xiàn)原理
如果要編寫可升級的智能合約,通常的做法是使用代理模式
來實現(xiàn)。用戶請求的是代理合約(Proxy Contract)
,再通過代理合約進行委托調(diào)用實際的邏輯合約(Logic Contract)
。因為是通過delegatecall
函數(shù)調(diào)用邏輯合約,實際上是由代理合約來存儲狀態(tài)變量,即它是存儲層。這就像你只是執(zhí)行了邏輯合約的程序,并在代理合約所在的上下文中存儲狀態(tài)變量。代理合約通常有兩種實現(xiàn)方式:透明代理,UUPS。這兩種方式最核心的區(qū)別在于智能合約升級的邏輯在哪里實現(xiàn),透明代理模式把升級的邏輯放在了代理合約里,而UUPS則放在了邏輯合約里。
示例代碼
在代理合約中,完成對實際的邏輯合約重定向的功能(setLogicAddress),以及通過委托調(diào)用,對主要函數(shù)的實現(xiàn)(setNumber)。
contract proxy {
uint256 private number;
address private logicAddress;
address private owner;
constructor(address _logicAddress) {
logicAddress = _logicAddress;
owner = msg.sender;
}
modifier ownerable() {
require(owner == msg.sender);
_;
}
function setLogicAddress(address _logicAddress) ownerable public {
logicAddress = _logicAddress;
}
function setNumber(uint256 _number) public returns(bool) {
(bool success,) = logicAddress.delegatecall(abi.encodeWithSignature("setNumber(uint256)", _number));
return success;
}
}
在第一個版本的邏輯合約中,我們實現(xiàn)的功能是對number進行+1操作,部署logic1
合約,調(diào)用proxy
合約中的setLogicAddress
方法,傳入logic1
合約的地址。
contract logic1 {
uint256 private number;
function setNumber(_number) public {
number = _number + 1;
}
function getNumber() public view returns(uint256) {
return number;
}
}
隨后需要升級,改為對number進行×2操作,部署logic2
合約,調(diào)用proxy
合約中的setLogicAddress
方法,傳入logic2
合約的地址,即可完成升級。
contract logic2 {
uint256 private number;
function setNumber(_number) public {
number = _number * 2;
}
function getNumber() public view returns(uint256) {
return number;
}
}
總結(jié)
智能合約的開發(fā)技術(shù)相對較新,暫未形成工業(yè)級的標準規(guī)范,開發(fā)者缺乏明確的指導(dǎo),不能保證所開發(fā)的代碼的安全性。另外,既然都是由人創(chuàng)造的,就會受限于主觀意識,一些人為因素也將會導(dǎo)致事故的發(fā)生。對于智能合約的安全驗證,暫未出現(xiàn)正式的并且廣泛使用的技術(shù)規(guī)范。
智能合約的安全性是區(qū)塊鏈技術(shù)的一個重要方面,也正是其復(fù)雜之處。智能合約在帶來諸多好處的同時,也容易受到各種潛在安全風(fēng)險和漏洞的影響。在開發(fā)基于區(qū)塊鏈的應(yīng)用程序時,智能合約的安全性是一個值得考慮的重要因素,必須采取積極主動的方法來識別和減少漏洞,以確保合約及其所管理資產(chǎn)的完整性和安全性。文章來源:http://www.zghlxwxcb.cn/news/detail-778495.html
轉(zhuǎn)載申明:未經(jīng)作者本人同意,本篇文章不可轉(zhuǎn)載或者作為文摘、資料刊登。文章來源地址http://www.zghlxwxcb.cn/news/detail-778495.html
到了這里,關(guān)于歡迎來到Web3.0的世界:Solidity智能合約安全漏洞分析的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!