如果智能合約被攻擊了怎麽辦

虽然區塊鏈和智能合约技术每天都在革新,但是风险依然很高。攻击者从没有放弃去寻找机会来攻击这些合约。

如果你在數字貨幣世界待過足夠時間,也許你聽說過1或2個智能合約攻擊時間,這些攻擊導致了幾千萬美元的盜竊損失。最著名的攻擊是DAO事件,這是數字貨幣世界最受期待的項目之一,同時也是智能合約的改革。雖然很多人聽說過這些攻擊,但是很少人知道到底發生了什麽,是怎麽發生的,以及如何避免這些錯誤。Y55顯卡之家

智能合約是動態的,複雜的以及難以置信地強大。雖然他們的潛力是很難想象,但是也不可能一夜之間就成爲了攻擊的對象。也就是說,對于往後的數字貨幣,我們可以從之前的錯誤中學到經驗,然後一起成長。雖然DAO是已經發生的事情,但是這對于開發者,投資者,以及社區成員對于智能合約攻擊來說,都是一個很好的例子。Y55顯卡之家

今天,我想和大家聊聊從DAO事件中,我們學到的3件事。Y55顯卡之家

攻擊#1:重入攻擊Y55顯卡之家

当攻击者通过对目标调用提款操作的时候,重入攻击就会发生,就好像DAO事件一样。当合约不能在发出资金之前更新状态(用户余额),攻击者就可以连续进行提取函数调用,来获得合约中的资金。任何时候攻击者获得以太币,他的合约都会自动地调用反馈函数,function (),这就再次调用了提现合约。这时候,攻击就会进入递归回路,这时候这个合约中的资金就会转入攻击者。因为目标合约都在不停地调用攻击者的函数,这个合约也不会更新攻击者的余额。当前的合约不会发现有任何问题,更清楚地说,合约函数中包含反馈函数,当合约收到以太币和零数据的时候,合约函数就会自动执行。Y55顯卡之家

如果智能合約被攻擊了怎麽辦Y55顯卡之家

攻擊流程Y55顯卡之家

1.攻擊者將以太幣存入目標函數Y55顯卡之家

2.目標函數就會根據存入的以太幣而更新攻擊者的約Y55顯卡之家

3.攻擊者請求拿回資金Y55顯卡之家

4.資金就會退回Y55顯卡之家

5.攻擊者的反饋函數生效,然後調用提現功能Y55顯卡之家

6.智能合約的邏輯就會更新攻擊者的余額,因爲提現又被成功調用Y55顯卡之家

7.資金發送到攻擊者Y55顯卡之家

8.第5-7步重複使用Y55顯卡之家

9.一旦攻擊結束,攻擊者就會把資金從他們自己的合約發送到個人地址Y55顯卡之家

1*UeDgMZo2n0skHzgkl352zQY55顯卡之家

重入攻擊的遞歸回路Y55顯卡之家

很不幸地是,一旦這個攻擊開始,無法停下。攻擊者的提現功能會被一次次地調用,直到合約中的燃料跑完,或者被害者的以太幣余額被消耗光。Y55顯卡之家

代碼Y55顯卡之家

下面就是DAO合约的简单版本,其中会包括一些介绍来为这些不熟悉代碼/ solidity语言更好地理解合约。Y55顯卡之家

contract babyDAO {Y55顯卡之家

/* assign key/value pair so we can look upY55顯卡之家

credit integers with an ETH address */Y55顯卡之家

mapping (address => uint256) public credit;Y55顯卡之家

/* a function for funds to be added to the contract,Y55顯卡之家

sender will be credited amount sent */Y55顯卡之家

function donate(address to) payable {Y55顯卡之家

credit[msg.sender] += msg.value;Y55顯卡之家

Y55顯卡之家

/*show ether credited to address*/Y55顯卡之家

function assignedCredit(address) returns (uint) {Y55顯卡之家

return credit[msg.sender];Y55顯卡之家

Y55顯卡之家

/*withdrawal ether from contract*/Y55顯卡之家

function withdraw(uint amount) {Y55顯卡之家

if (credit[msg.sender] >= amount) {Y55顯卡之家

msg.sender.call.value(amount)();Y55顯卡之家

credit[msg.sender] -= amount;Y55顯卡之家

Y55顯卡之家

Y55顯卡之家

Y55顯卡之家

如果我们看下函数withdraw(),我们可以看到DAO合约使用address.call.value()来发送资金到msg.sender。不仅如此,在资金发出后,合约会更新credit[msg.sender]的状态。攻击者在发现了合约代碼中的问题,就能够使用类似下面的ThisIsAHodlUp {}来将资金转入contract babyDAO{}合约。Y55顯卡之家

import ‘browser/babyDAO.sol’;Y55顯卡之家

contract ThisIsAHodlUp {Y55顯卡之家

/* assign babyDAO contract as "dao" */Y55顯卡之家

babyDAO public dao = babyDAO(0x2ae...);Y55顯卡之家

address owner;Y55顯卡之家

/*assign contract creator as owner*/Y55顯卡之家

constructor(ThisIsAHodlUp) public {Y55顯卡之家

owner = msg.sender;Y55顯卡之家

Y55顯卡之家

/*fallback function, withdraws funds from babyDAO*/Y55顯卡之家

function() public {Y55顯卡之家

dao.withdraw(dao.assignedCredit(this));Y55顯卡之家

Y55顯卡之家

/*send drained funds to attacker’s address*/Y55顯卡之家

function drainFunds() payable public{Y55顯卡之家

owner.transfer(address(this).balance);Y55顯卡之家

Y55顯卡之家

Y55顯卡之家

需要注意地是,這個後退函數,function(),會調用DAO或者babyDAO{}的提現函數,來從合約中盜取資金。從另個方面來說,當攻擊者想要把所有偷竊來的資金賺到他們的地址,drainFunds()功能會被調用。Y55顯卡之家

解決方案Y55顯卡之家

现在,我们应该清楚重放攻击会利用两个特别的智能合约漏洞。第一个是当合约的状态在资金发出之后,而不是之前进行更新。由于在发出资金前无法更新合约状态,函数就会在中间计算的时候被打断,合约也认为资金其实还没有发出。第二个漏洞就当合约错误地使用address.call.value()来发出资金,而不是address.transfer() 或者 address.send()。这两个都受限于2300gas,只记录一个事件而不是多个外部调用。Y55顯卡之家

contract babyDAO{Y55顯卡之家

....Y55顯卡之家

function withdraw(uint amount) {Y55顯卡之家

if (credit[msg.sender] >= amount) {Y55顯卡之家

credit[msg.sender] -= amount; /* updates balance first */Y55顯卡之家

msg.sender.send(amount)(); /* send funds properly */Y55顯卡之家

Y55顯卡之家

Y55顯卡之家

攻擊2:下溢攻擊Y55顯卡之家

虽然DAO合约不会让受害者掉入下溢攻击,我们能够通过现有的babyDAO contract{}来更好地理解这些攻击为什么会发生。Y55顯卡之家

首先,我们需要理解什么是256单位制。一个256单位制是由256个字节组成。以太坊的虚拟机是使用256字节来完成的。因为以太坊虚拟机受限于256字节的大小,所以数字的范围是0到4,294,967,295 (22??)。如果我们超过这个范围,那么数字就会重置到范围的最底部(22?? + 1 = 0)。如果我们低于这个范围,这个数字就会重置到这个范围的顶端(0–1= 22??)。Y55顯卡之家

當我們從零中減去大于零的數,就會發生下溢攻擊,導致一個新的22??數集。現在,如果攻擊者的余額發生了下溢,那麽這部分余額就會更新,從而導致整個資金被盜。Y55顯卡之家

攻擊流程Y55顯卡之家

攻擊者通過發出1Wei到目標合約,來啓動攻擊。Y55顯卡之家

合約認證發出資金的人Y55顯卡之家

隨後調用1Wei的提現函數Y55顯卡之家

合約會從發送者的賬戶扣除的1Wei,現在賬戶余額又是零Y55顯卡之家

因爲目標合約將以太幣發給攻擊者,攻擊者的退回函數被處罰,所以提現函數又被調用。Y55顯卡之家

提現1Wei的事件被記錄Y55顯卡之家

攻擊者合約的余額就會更新兩次,第一次是到零,第二次是到-1。Y55顯卡之家

攻擊者的余額回置到22??Y55顯卡之家

攻擊者通過提現目標合約的所有資金,從而完成整個攻擊Y55顯卡之家

代碼Y55顯卡之家

/*donate 1 wei, withdraw 1 wei*/Y55顯卡之家

function attack() {Y55顯卡之家

dao.donate.value(1)(this);Y55顯卡之家

dao.withdraw(1);Y55顯卡之家

Y55顯卡之家

/*fallback function, results in 0–1 = 2**256 */Y55顯卡之家

function() {Y55顯卡之家

if (performAttack) {Y55顯卡之家

performAttack = false;Y55顯卡之家

dao.withdraw(1);Y55顯卡之家

Y55顯卡之家

Y55顯卡之家

/*extract balance from smart contract*/Y55顯卡之家

function getJackpot() {Y55顯卡之家

dao.withdraw(dao.balance);Y55顯卡之家

owner.send(this.balance);Y55顯卡之家

Y55顯卡之家

Y55顯卡之家

解決方案Y55顯卡之家

为了防止受害人陷入下溢攻击,最好的方法是看更新的状态是否在字节范围内。我们可以添加参数来检查我们的代碼,作为最后一层保护。函数withdraw()的首行代碼是为了检查是否有足够的资金,第二行是为了检查超溢,第三个是检查下溢。Y55顯卡之家

contract babysDAO{Y55顯卡之家

....Y55顯卡之家

/*withdrawal ether from contract*/Y55顯卡之家

function withdraw(uint amount) {Y55顯卡之家

if (credit[msg.sender] >= amountY55顯卡之家

&& credit[msg.sender] + amount >= credit[msg.sender]Y55顯卡之家

&& credit[msg.sender] - amount <= credit[msg.sender]) {Y55顯卡之家

credit[msg.sender] -= amount;Y55顯卡之家

msg.sender.send(amount)();Y55顯卡之家

Y55顯卡之家

Y55顯卡之家

需要注意,就像我们之前讨论,我们上面的代碼是在发出资金之前更新用户的余额。Y55顯卡之家

攻擊#3:跨函數競爭條件Y55顯卡之家

最后要说的,就是跨函数竞争攻击。就像在重放攻击中所说,DAO合约不能正确的更新合约状态,并且可以让资金被盗窃。DAO问题和外部调用中的部分原因是跨函数竞争条件攻击的潜在原因。虽然以太坊中所有的转账是线性发生(一个在另一个后面), 外部调用(另一个合约或者地址的调用)如果没有被合理管理,就会成为灾难的导火线。在现实世界中,他们是完全可以避免的。当两个函数被调用并且分享同个状态,跨函数竞争条件攻击就会发生。这个合约就会想到,现在有两个合约状态存在,但是现实是只有一个真正的合约状态存在。我们不能同时获得X = 3和X = 4这两种结果。 让我们用一个例子来说明这个内容。Y55顯卡之家

攻击和代碼Y55顯卡之家

contract crossFunctionRace{Y55顯卡之家

mapping (address => uint) private userBalances;Y55顯卡之家

/* uses userBalances to transfer funds */Y55顯卡之家

function transfer(address to, uint amount) {Y55顯卡之家

if (userBalances[msg.sender] >= amount) {Y55顯卡之家

userBalances[to] += amount;Y55顯卡之家

userBalances[msg.sender] -= amount;Y55顯卡之家

Y55顯卡之家

Y55顯卡之家

/* uses userBalances to withdraw funds */Y55顯卡之家

function withdrawalBalance() public {Y55顯卡之家

uint amountToWithdraw = userBalances[msg.sender];Y55顯卡之家

require(msg.sender.send(amountToWithdraw)());Y55顯卡之家

userBalances[msg.sender] = 0;Y55顯卡之家

Y55顯卡之家

Y55顯卡之家

上面的合约有2个功能 – 一个是可以转移资金,另一个是提现资金。我们假设攻击者调用了函数transfer(),然后同时使用外部调用函数withdrawalBalance()。userBalance[msg.sender]的状态通过2个不同的方向被抽出。用户的余额还没有被设为0,但是尽管资金已经被提取,攻击者也能够转移资金。这样情况下,合约可以让攻击者使用双花,这也是區塊鏈技术想要解决的问题之一。Y55顯卡之家

注意:如果有函數分享狀態,跨函數競爭條件攻擊就會在多個合約中發生。Y55顯卡之家

-在調用外部函數之前,應該完成所有的內部工作Y55顯卡之家

-避免發生外部調用Y55顯卡之家

-在不可避免地時候,使用外部函數“不可信”Y55顯卡之家

-在外部調用不可避免的情況下,使用互斥Y55顯卡之家

根据下面的合约,我们可以看到一个例子1) 在完成外部调用之前,完成内部工作。2)将所有外部调用都设为“不可信”。我们的合约会让资金发送到一个地址,并且允许用户一次性将资金存入合同。Y55顯卡之家

contract crossFunctionRace{Y55顯卡之家

mapping (address => uint) private userBalances;Y55顯卡之家

mapping (address => uint) private reward;Y55顯卡之家

mapping (address => bool) private claimedReward;Y55顯卡之家

//makes external call, need to mark as untrustedY55顯卡之家

function untrustedWithdraw(address recipient) public {Y55顯卡之家

uint amountWithdraw = userBalances[recipient];Y55顯卡之家

reward[recipient] = 0;Y55顯卡之家

require(recipient.call.value(amountWithdraw)());Y55顯卡之家

Y55顯卡之家

//untrusted because withdraw is called, an external callY55顯卡之家

function untrustedGetReward(address recipient) public {Y55顯卡之家

//check that reward hasn’t already been claimedY55顯卡之家

require(!claimedReward[recipient]);Y55顯卡之家

//internal work first (claimedReward and assigning reward)Y55顯卡之家

claimedReward = true;Y55顯卡之家

reward[recipient] += 100;Y55顯卡之家

untrustedWithdraw(recipient);Y55顯卡之家

Y55顯卡之家

Y55顯卡之家

我們可以看出,這個合約的首個函數在發送資金到用戶的合約/地址的時候,就會發生外部調用。同樣地,獎勵函數在發送一次性獎勵的時候,也會使用提現函數,因爲這也是不可信的。同樣重要地是,合約需要執行所有內部工作。就好像重入攻擊,函數untrustedGetReward()會在允許提現之前,讓用戶獲得一次性的獎勵,從而防止跨函數競爭條件攻擊。Y55顯卡之家

在真實世界,智能合約不需要依賴于外部調用。事實上,外部調用在很多情況下,在工作環境中都幾乎不可能發生的。由于這個原因,使用互斥體來“鎖定”一些狀態,並且讓擁有者有能力去改變狀態,可以幫助防止這類災難。雖然互斥體非常有效,但是當用于多個合約的時候,都會變的很棘手。如果你使用互斥體來防止這類攻擊,你需要很仔細地確保沒有其他方法來鎖定,或者永遠不會釋放。如果使用互斥體的方法,在寫入智能合約的時候,你需要保證你完全理解潛在的危險。Y55顯卡之家

contract mutexExample{Y55顯卡之家

mapping (address => uint) private balances;Y55顯卡之家

bool private lockBalances;Y55顯卡之家

function deposit() payable public returns (bool) {Y55顯卡之家

/*check if lockBalances is unlocked before proceeding*/Y55顯卡之家

require(!lockBalances);Y55顯卡之家

/*lock, execute, unlock */Y55顯卡之家

lockBalances = true;Y55顯卡之家

balances[msg.sender] += msg.value;Y55顯卡之家

lockBalances = false;Y55顯卡之家

return true;Y55顯卡之家

Y55顯卡之家

function withdraw(uint amount) payable public returns (bool) {Y55顯卡之家

/*check if lockBalances is unlocked before proceeding*/Y55顯卡之家

require(!lockBalances && amount > 0 && balances[msg.sender]Y55顯卡之家

>= amount);Y55顯卡之家

/*lock, execute, unlock*/Y55顯卡之家

lockBalances = true;Y55顯卡之家

if (msg.sender.call(amount)()) {Y55顯卡之家

balances[msg.sender] -= amount;Y55顯卡之家

Y55顯卡之家

lockBalances = false;Y55顯卡之家

return true;Y55顯卡之家

Y55顯卡之家

Y55顯卡之家

以上,我們可以看到合約mutexExample()會有私人鎖定狀態,來實行deposit()函數功能和withdraw()函數。鎖定會防止用戶能夠在所有的初步調用完成之前,成功完成withdraw()調用,可以防止任何種類的跨函數競爭條件攻擊。Y55顯卡之家

最後的結果Y55顯卡之家

力量越大,责任越大。虽然區塊鏈和智能合约技术每天都在革新,但是风险依然很高。攻击者从没有放弃去寻找机会来攻击这些合约。这取决于我们来保证,我们可以从之前项目的问题中学习经验,来让我们获得成长。希望通过这篇文章,以及其他系列文章,你可以更明白智能合约攻击。Y55顯卡之家

責任編輯:ctY55顯卡之家

相關推薦