最近,智能合約漏洞很火吟温。
讓我們再來看一下4月22日BeautyChain(BEC)的智能合約中一個毀滅性的漏洞序仙。
BeautyChain團隊宣布,BEC代幣在4月22日出現(xiàn)異常鲁豪。攻擊者通過智能合約漏洞成功轉(zhuǎn)賬了10^58 BEC到兩個指定的地址潘悼。
具體交易詳情https://etherscan.io/tx/0xad89ff16fd1ebe3a0a7cf4ed282302c06626c1af33221ebe0d3a470aba4a660f
攻擊者到底是怎么攻擊的?為什么能轉(zhuǎn)賬這么大的BEC爬橡?
智能合約代碼
首先我們來看BEC轉(zhuǎn)賬的智能合約代碼
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語言治唤,是一門面向合約的,為實現(xiàn)智能合約而創(chuàng)建的高級編程語言糙申。
變量類型
在讀代碼之前我們先來簡單了解一下以下幾個變量類型(Solidity):
address
160位的值宾添,且不允許任何算數(shù)操作。
unit 8
8位無符號整數(shù),范圍是0到2^8減1?(0-255)
unit256
256位無符號整數(shù)缕陕,范圍是0到2^256減1
(0-115792089237316195423570985008687907853269984665640564039457584007913129639935)
敲黑板粱锐,玩手機的同學注意看這里,這里是考試重點哦
那么扛邑,我們請看如下神奇的化學反應(yīng)
定義變量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)
函數(shù)有兩個參數(shù):
_receivers —————轉(zhuǎn)賬接收人毅该,address類型的變量數(shù)組博秫,是一個160位的值。
_value ———————-轉(zhuǎn)賬數(shù)量眶掌,uint256的狀態(tài)變量挡育,256位的無符號整數(shù)。
定義函數(shù)batchTransfer朴爬,功能主要是實現(xiàn)轉(zhuǎn)賬即寒,接收兩個參數(shù),定義了參數(shù)的取值范圍召噩。
第二行
uint cnt = _receivers.length;
計算接收人地址對應(yīng)地址數(shù)組的長度母赵,即轉(zhuǎn)賬給多少人。
第三行
uint256 amount = uint256(cnt) * _value;
把unit類型的cnt參數(shù)值強制轉(zhuǎn)換為uint256然后乘以轉(zhuǎn)賬數(shù)量_value 并賦值給uint256類型的amount變量具滴。
第四行
require(cnt > 0 && cnt <= 20);
require函數(shù)
require的入?yún)⑴卸?b>false凹嘲,則終止函數(shù),恢復所有對狀態(tài)和以太幣賬戶的變動构韵,并且也不會消耗 gas 周蹭。
判斷cnt是否大于0且cnt是否小于等于20
第五行
require(_value > 0 && balances[msg.sender] >= amount);
參數(shù)解讀:
_value—————————————轉(zhuǎn)賬數(shù)量
balances[msg.sender]————-轉(zhuǎn)賬人余額
amount————————————轉(zhuǎn)賬總數(shù)量
判斷_value是否大于0且轉(zhuǎn)賬人的余額balances[msg.sender]大于等于轉(zhuǎn)賬總金額amount
第六行
balances[msg.sender] = balances[msg.sender].sub(amount);
計算轉(zhuǎn)賬人的余額趋艘,使用當前余額balances[msg.sender]減去轉(zhuǎn)賬總數(shù)量
第七行
for (uint i = 0; i < cnt; i++) {
這里是一個循環(huán),循環(huán)次數(shù)為cnt(遍歷轉(zhuǎn)賬地址)
第八行
balances[_receivers[i]] = balances[_receivers[i]].add(_value);
當i有具體的值時凶朗,balances[_receivers[i]]表示轉(zhuǎn)賬接收人瓷胧,這里是表示轉(zhuǎn)賬人給轉(zhuǎn)賬接收人_value數(shù)量的幣。
第九行
Transfer(msg.sender, _receivers[i], _value);
保存轉(zhuǎn)賬記錄
第十行
return true;
函數(shù)范圍為True
代碼流程
OK俱尼,我們讀了完整的代碼抖单,接下來請看一個流程圖
函數(shù)的流程是這樣,那么攻擊者到底是怎么攻擊的呢遇八?他為什么這么秀?同樣都是九年義務(wù)教育……
攻擊過程
其實耍休,他只是細心了一點刃永,所使用的攻擊方法并不高明啊,你且聽我慢慢道來羊精,注意看斯够,別走神啊。
交易詳情
我們首先看這筆詳細的交易:
好了喧锦,我們從圖可以看到
轉(zhuǎn)賬接收人有兩個地址读规,即balances[_receivers]:
000000000000000000000000b4d30cac5124b46c2df0cf3e3e1be05f42119033
0000000000000000000000000e823ffe018727585eaf5bc769fa80472f76c3d7
轉(zhuǎn)賬數(shù)量為_value:
8000000000000000000000000000000000000000000000000000000000000000(十六進制)
轉(zhuǎn)10進制為
57896044618658097711785492504343953926634992332820282019728792003956564819968
實戰(zhàn)
OK,接下來我們來走函數(shù)流程
第一行
function batchTransfer(address[] _receivers, uint256 _value) public whenNotPaused returns (bool)
正常執(zhí)行
第二行
uint cnt = _receivers.length
由于這里有兩個轉(zhuǎn)賬接收人地址燃少,address數(shù)組長度為2束亏,所以cnt為2,類型為uint
第三行
uint256 amount = uint256(cnt) * _value;
_value=57896044618658097711785492504343953926634992332820282019728792003956564819968
cnt=2
兩者相乘得到amount阵具,類型為uint256
即amount=115792089237316195423570985008687907853269984665640564039457584007913129639936
考試重點用上了碍遍,記不住的同學去前面看看。
amount的類型為uint256阳液,那么按照理論怕敬,它的最大取值是0到2^256減1,即
115792089237316195423570985008687907853269984665640564039457584007913129639935
所以A泵蟆6颉!
amount瞬間從115792089237316195423570985008687907853269984665640564039457584007913129639936變成了0?
第三行得到的結(jié)果:amount=0
第四行
require(cnt > 0 && cnt <= 20);
cnt=2鹰溜,2肯定大于0虽填,2當然也小于等于20
所以這個條件成立,require函數(shù)返回值為True奉狈。
第五行
require(_value > 0 && balances[msg.sender] >= amount);
_value=57896044618658097711785492504343953926634992332820282019728792003956564819968
_value肯定是大于0卤唉,轉(zhuǎn)賬人的余額balances[msg.sender]肯定是大于等于0的。
所以這個條件同樣成立仁期,require函數(shù)返回值為True桑驱。
第六行
balances[msg.sender] = balances[msg.sender].sub(amount);
前面的條件都成立竭恬,那么代碼會執(zhí)行到這。
這行代碼是求轉(zhuǎn)賬人轉(zhuǎn)完賬以后剩下的余額熬的,amount為0 痊硕,那么轉(zhuǎn)賬人的余額其實沒變!Q嚎颉岔绸!
第七行
for (uint i = 0; i < cnt; i++)
cnt=2,該行代碼表示執(zhí)行兩次后面的操作
第八行
balances[_receivers[i]] = balances[_receivers[i]].add(_value);
i=0時橡伞,轉(zhuǎn)賬接收人balances[_receivers[0]]的余額加_value
i=1時盒揉,轉(zhuǎn)賬接收人balances[_receivers[1]]的余額加_value
看到這里其實我們就很明白了吧。
攻擊者給以下兩個轉(zhuǎn)賬接收人
000000000000000000000000b4d30cac5124b46c2df0cf3e3e1be05f42119033
0000000000000000000000000e823ffe018727585eaf5bc769fa80472f76c3d7
轉(zhuǎn)了
_value=57896044618658097711785492504343953926634992332820282019728792003956564819968個幣
更可惡的是兑徘,攻擊者執(zhí)行完這個操作刚盈,轉(zhuǎn)賬人的余額根本沒變,看代碼第六行的執(zhí)行結(jié)果挂脑。
第九行
Transfer(msg.sender, _receivers[i], _value);
這里只是把上面兩個轉(zhuǎn)賬記錄保存藕漱。
第十行
return true;
函數(shù)返回為True
小結(jié)
千里之堤毀于蟻穴!
就一個溢出漏洞崭闲,導致BEC的市值瞬間變0
這么傻的問題肋联,寫代碼的人是寫睡著了嗎?刁俭?橄仍?
不,其實他根本沒睡著啊薄翅,人家還用了SafeMath里的add函數(shù)和sub函數(shù)
我們看看什么是SafeMath函數(shù)
/**
* @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;
?}
這里是乘法計算沙兰,計算出乘法的結(jié)果后會用assert函數(shù)去驗證結(jié)果是否正確。
回到我們前面的dis第三行代碼執(zhí)行后的結(jié)果
_value=57896044618658097711785492504343953926634992332820282019728792003956564819968
cnt=2
兩者相乘得到amount翘魄,類型為uint256
由于溢出鼎天,amount=0
賦值給mul函數(shù)即
c=amount,而amount=0暑竟,則c=0
a=cnt, 而cnt=2斋射,則a=2
b=_value
得出
b=57896044618658097711785492504343953926634992332820282019728792003956564819968
那么c/a==b這個式子不成立,導致assert函數(shù)執(zhí)行會報錯但荤,assert報錯罗岖,那么就不會執(zhí)行后面的代碼,也就不會發(fā)生溢出腹躁。
也就是說桑包,寫這段代碼的人,加減法他用了SafeMath里面的add函數(shù)和sub函數(shù)纺非,但是卻沒有用里面的乘法函數(shù)mul
如何防止這樣的漏洞哑了?
肯定是要用SafeMath函數(shù)啊赘方,你加減法用了,乘法不用弱左,你咋這么皮呢
代碼上線前要做代碼審計啊親窄陡,強調(diào)多少遍了!
合理使用變量類型拆火,了解清楚變量的范圍
一定要考慮到溢出跳夭!一定要考慮到溢出!一定要考慮到溢出们镜!重要的事情說三遍币叹。
原文地址:https://mp.weixin.qq.com/s?__biz=MzU0NDg1MjQ0Nw==&mid=2247483672&idx=1&sn=2d7aad9e1645c7218161f8561664c102&chksm=fb7491a8cc0318be802db628097bf3da59cccf979fbc4af498e0209abb5bee3b17b9ccfd0f5b&mpshare=1&scene=1&srcid=0531lVTfE6DeC04XZTF6c8qu#rd