通俗易懂談BEC智慧合約致命漏洞

FLy_鵬程萬里發表於2018-06-19

安全事件

最近,智慧合約漏洞很火。
讓我們再來看一下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函式啊,你加減法用了,乘法不用,你咋這麼皮呢

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

合理使用變數型別,瞭解清楚變數的範圍

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

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

參考

相關文章