原文來自安全客,作者:區塊鏈威脅情報
原文鏈接:https://www.anquanke.com/post/id/146702

安全事件

最近,智能合約漏洞很火。

讓我們再來看一下4月22日BeautyChain(BEC)的智能合約中一個毀滅性的漏洞。

BeautyChain團隊宣布,BEC代幣在4月22日出現異常。攻擊者通過智能合約漏洞成功轉賬了10^58 BEC到兩個指定的地址。

具體交易詳情https://etherscan.io/tx/0xad89ff16fd1ebe3a0a7cf4ed282302c06626c1af33221ebe0d3a470aba4a660f

攻擊者到底是怎么攻擊的?為什么能轉賬這么大的BEC?

智能合約代碼

首先我們來看BEC轉賬的智能合約代碼

function batchTransfer(address[] _receivers, uint256 _value) public whenNotPaused returns (bool) {
    uint cnt = _receivers.length;
    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;
 }

以上的代碼是Solidity語言,是一門面向合約的,為實現智能合約而創建的高級編程語言。

變量類型

在讀代碼之前我們先來簡單了解一下以下幾個變量類型(Solidity):

address
160位的值,且不允許任何算數操作。

uint 8
8位無符號整數,范圍是0到2^8減1 (0-255)

uint256
256位無符號整數,范圍是0到2^256減1
(0-115792089237316195423570985008687907853269984665640564039457584007913129639935)

敲黑板,玩手機的同學注意看這里,這里是考試重點哦

那么,我們請看如下神奇的化學反應

定義變量uint a

a的取值范圍是0到255

當a=255,我們對a加 1,a會變成 0。
當a=255,我們對a加 2,a會變成 1。
當a=0,我們對a減 1,a會變成 255。
當a=0,我們對a減 2,a會變成 255。

a的值超過了它實際的取值范圍,然后會得出后面的值,這種情況叫溢出。

代碼解讀

知道了這幾個變量類型,下面我們一行一行的來讀這段代碼。

第一行

function batchTransfer(address[] _receivers, uint256 _value) public whenNotPaused returns (bool)

函數有兩個參數:

_receivers —————轉賬接收人,address類型的變量數組,是一個160位的值。
_value ———————-轉賬數量,uint256的狀態變量,256位的無符號整數。

定義函數batchTransfer,功能主要是實現轉賬,接收兩個參數,定義了參數的取值范圍。

第二行

uint cnt = _receivers.length;

計算接收人地址對應地址數組的長度,即轉賬給多少人。

第三行

uint256 amount = uint256(cnt) * _value;

把unit類型的cnt參數值強制轉換為uint256然后乘以轉賬數量_value 并賦值給uint256類型的amount變量。

第四行

require(cnt > 0 && cnt <= 20);

require函數

require的入參判定為 false,則終止函數,恢復所有對狀態和以太幣賬戶的變動,并且也不會消耗 gas 。 判斷cnt是否大于0且cnt是否小于等于20

第五行

require(_value > 0 && balances[msg.sender] >= amount);

參數解讀:

_value—————————————轉賬數量
balances[msg.sender]————-轉賬人余額
amount————————————轉賬總數量

判斷_value是否大于0且轉賬人的余額balances[msg.sender]大于等于轉賬總金額amount

第六行

balances[msg.sender] = balances[msg.sender].sub(amount);
計算轉賬人的余額,使用當前余額balances[msg.sender]減去轉賬總數量

第七行

for (uint i = 0; i < cnt; i++) {
這里是一個循環,循環次數為cnt(遍歷轉賬地址)

第八行

balances[_receivers[i]] = balances[_receivers[i]].add(_value);
當i有具體的值時,balances[_receivers[i]]表示轉賬接收人,這里是表示轉賬人給轉賬接收人_value數量的幣。

第九行

Transfer(msg.sender, _receivers[i], _value);
保存轉賬記錄

第十行

return true;
函數返回為True

代碼流程

OK,我們讀了完整的代碼,接下來請看一個流程圖

函數的流程是這樣,那么攻擊者到底是怎么攻擊的呢?他為什么這么秀?同樣都是九年義務教育……

攻擊過程

其實,他只是細心了一點,所使用的攻擊方法并不高明啊,你且聽我慢慢道來,注意看,別走神啊。

交易詳情

我們首先看這筆詳細的交易:

好了,我們從圖可以看到

轉賬接收人有兩個地址,即balances[_receivers]:

000000000000000000000000b4d30cac5124b46c2df0cf3e3e1be05f42119033
0000000000000000000000000e823ffe018727585eaf5bc769fa80472f76c3d7

轉賬數量為_value:

8000000000000000000000000000000000000000000000000000000000000000(十六進制)

轉10進制為

57896044618658097711785492504343953926634992332820282019728792003956564819968
實戰

OK,接下來我們來走函數流程

第一行

function batchTransfer(address[] _receivers, uint256 _value) public whenNotPaused returns (bool)
正常執行

第二行

uint cnt = _receivers.length
由于這里有兩個轉賬接收人地址,address數組長度為2,所以cnt為2,類型為uint

第三行

uint256 amount = uint256(cnt) * _value;

_value=57896044618658097711785492504343953926634992332820282019728792003956564819968
cnt=2

兩者相乘得到amount,類型為uint256

amount=115792089237316195423570985008687907853269984665640564039457584007913129639936

考試重點用上了,記不住的同學去前面看看。

amount的類型為uint256,那么按照理論,它的最大取值是0到2^256減1,即

115792089237316195423570985008687907853269984665640564039457584007913129639935

所以!!!

amount瞬間從115792089237316195423570985008687907853269984665640564039457584007913129639936變成了0

第三行得到的結果:amount=0

第四行

require(cnt > 0 && cnt <= 20);

cnt=2,2肯定大于0,2當然也小于等于20

所以這個條件成立,require函數返回值為True。

第五行

require(_value > 0 && balances[msg.sender] >= amount);

_value=57896044618658097711785492504343953926634992332820282019728792003956564819968

_value肯定是大于0,轉賬人的余額balances[msg.sender]肯定是大于等于0的。

所以這個條件同樣成立,require函數返回值為True。

第六行

balances[msg.sender] = balances[msg.sender].sub(amount); 前面的條件都成立,那么代碼會執行到這。

這行代碼是求轉賬人轉完賬以后剩下的余額,amount為0 ,那么轉賬人的余額其實沒變!!!

第七行

for (uint i = 0; i < cnt; i++)

cnt=2,該行代碼表示執行兩次后面的操作

第八行
balances[_receivers[i]] = balances[_receivers[i]].add(_value);
i=0時,轉賬接收人balances[_receivers[0]]的余額加_value
i=1時,轉賬接收人balances[_receivers[1]]的余額加_value

看到這里其實我們就很明白了吧。

攻擊者給以下兩個轉賬接收人

000000000000000000000000b4d30cac5124b46c2df0cf3e3e1be05f42119033
0000000000000000000000000e823ffe018727585eaf5bc769fa80472f76c3d7

轉了

_value=57896044618658097711785492504343953926634992332820282019728792003956564819968個幣

更可惡的是,攻擊者執行完這個操作,轉賬人的余額根本沒變,看代碼第六行的執行結果。

第九行

Transfer(msg.sender, _receivers[i], _value);

這里只是把上面兩個轉賬記錄保存。

第十行

return true;

函數返回為True

小結

千里之堤毀于蟻穴!

就一個溢出漏洞,導致BEC的市值瞬間變0

這么傻的問題,寫代碼的人是寫睡著了嗎???

不,其實他根本沒睡著啊,人家還用了SafeMath里的add函數和sub函數

我們看看什么是SafeMath函數

/**
 * @title SafeMath
 * @dev Math operations with safety checks that throw on error
 */
library SafeMath {
  function mul(uint256 a, uint256 b) internal constant returns (uint256) {
    uint256 c = a * b;
    assert(a == 0 || c / a == b);
    return c;
  }
  function div(uint256 a, uint256 b) internal constant returns (uint256) {
    // assert(b > 0); // Solidity automatically throws when dividing by 0
    uint256 c = a / b;
    // assert(a == b * c + a % b); // There is no case in which this doesn’t hold
    return c;
  }

  function sub(uint256 a, uint256 b) internal constant returns (uint256) {
    assert(b <= a);
    return a – b;
  }

  function add(uint256 a, uint256 b) internal constant returns (uint256) {
    uint256 c = a + b;
    assert(c >= a);
    return c;
  }
}

注意看這一段

 function mul(uint256 a, uint256 b) internal constant returns (uint256) {
    uint256 c = a * b;
    assert(a == 0 || c / a == b);
    return c;
  }

這里是乘法計算,計算出乘法的結果后會用assert函數去驗證結果是否正確。

回到我們前面的dis第三行代碼執行后的結果

_value=57896044618658097711785492504343953926634992332820282019728792003956564819968
cnt=2

兩者相乘得到amount,類型為uint256

由于溢出,amount=0

賦值給mul函數即

c=amount,而amount=0,則c=0
a=cnt, 而cnt=2,則a=2
b=_value
得出
b=57896044618658097711785492504343953926634992332820282019728792003956564819968

那么c/a==b這個式子不成立,導致assert函數執行會報錯,assert報錯,那么就不會執行后面的代碼,也就不會發生溢出。

也就是說,寫這段代碼的人,加減法他用了SafeMath里面的add函數和sub函數,但是卻沒有用里面的乘法函數mul

如何防止這樣的漏洞?

肯定是要用SafeMath函數啊,你加減法用了,乘法不用,你咋這么皮呢

代碼上線前要做代碼審計啊親,強調多少遍了!

合理使用變量類型,了解清楚變量的范圍

一定要考慮到溢出!一定要考慮到溢出!一定要考慮到溢出!重要的事情說三遍。

寫這么通俗易懂,你應該看懂了吧??看懂了就給點個贊唄!

參考


本文經安全客授權發布,轉載請聯系安全客平臺。


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/615/