通俗易懂談BEC智慧合約致命漏洞
安全事件
最近,智慧合約漏洞很火。
讓我們再來看一下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=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函式啊,你加減法用了,乘法不用,你咋這麼皮呢
程式碼上線前要做程式碼審計啊親,強調多少遍了!
合理使用變數型別,瞭解清楚變數的範圍
一定要考慮到溢位!一定要考慮到溢位!一定要考慮到溢位!重要的事情說三遍。
寫這麼通俗易懂,你應該看懂了吧??看懂了就給點個讚唄!
參考
相關文章
- 美鏈BEC合約漏洞技術分析
- 什麼是智慧合約漏洞?
- DASP智慧合約Top10漏洞
- EduCoin智慧合約transferFrom任意轉賬漏洞
- 以太坊智慧合約 Hexagon 存在溢位漏洞Go
- ERC20 智慧合約整數溢位系列漏洞披露
- 保險智慧合約
- Java又爆致命漏洞Java
- 智慧合約從入門到精通:智慧合約的前世今生
- 編寫智慧合約
- iOS 部署智慧合約iOS
- 這些智慧合約漏洞,可能會影響你的賬戶安全!
- 富士通推出新技術檢測以太坊智慧合約漏洞
- 智慧合約從入門到精通:智慧合約的應用場景
- 報告指出34,000份以太坊智慧合約存在安全漏洞
- 智慧合約初體驗
- 如何實施智慧合約?
- NEO智慧合約白皮書
- 智慧合約安全性
- 什麼是智慧合約?
- 什麼是智慧合約
- 智慧合約是什麼
- Dapp 合約代幣系統開發智慧合約APP
- 智慧合約開發環境搭建及Hello World合約開發環境
- 以太坊智慧合約開發:讓合約接受轉賬
- 以太坊智慧合約開發第四篇:實現Hello World智慧合約
- 以太坊蜜罐智慧合約分析
- 3.06 EOS智慧合約(上)
- 3.08 EOS智慧合約(下)
- 2.06 hyperledger fabric智慧合約
- 使用remix ethereum部署智慧合約REM
- 通過Mist部署智慧合約
- 區塊鏈2.0:智慧合約區塊鏈
- 智慧合約開發神器-RemixREM
- 如何建立智慧合約遊戲系統?智慧合約遊戲開發核心原始碼示例遊戲開發原始碼
- Coinbase智慧合約被曝漏洞,使用者可無限量竊取以太幣
- DAPP智慧合約/系統開發/智慧合約原始碼/DAPP/Defi/NFT/IDOAPP原始碼
- 智慧合約Dapp專案如何開發構建?智慧合約開發原始碼示例APP原始碼