最近在幣圈很多人討論的BEC代碼漏洞柜去,損失了64億人民幣。那黑客是如何做到的拆宛?其實是通過bec代幣合約的整型溢出漏洞嗓奢,讓自己的地址憑空產生了大量的bec代幣。
什么是整型溢出
那什么是整型溢出呢浑厚?在solidity編寫合約時蔓罚,定義整型一般是用uint8, uint256。一個變量如果定義為uint8表示的無符號的8位整型瞻颂,即表示的范圍為0-255豺谈。當給這個變量賦值256時,即整型溢出變成了0贡这,以此類推257變成了1茬末。
下面通過合約代碼實例說明:
pragma solidity ^0.4.21;
contract HelloWorld{
function add(uint8 a, uint8 b) returns (uint8){
uint8 result = a + b;
return result;
}
}
這個合約代碼很簡單,將傳入的兩個整數相加盖矫,但是我定義的返回類型是uint8丽惭,即最多表示255蔚携。
這時我們傳入參數255和1键科,即255+1剪廉,按照我們前面說的姨伟,這時會出現整型溢出蛙紫,result為0唉堪。
通過remix 執(zhí)行add函數結果也為0炸卑。
BEC源碼分析
回到BEC的問題上亡蓉,它的問題也是類似的证芭,只不過BEC合約是uint256的整型溢出瞳浦。先看一下這筆漏洞的交易:https://etherscan.io/tx/0xad89ff16fd1ebe3a0a7cf4ed282302c06626c1af33221ebe0d3a470aba4a660f
可以看到這筆交易是通過調用bec合約的方法,分別轉了57,896,044,618,658,100,000,000,000,000,000,000,000,000,000,000,000,000,000,000.792003956564819968到兩個賬戶废士。
通過Input Data 可以看出是調用了batchTransfer方法實現bec代幣的轉賬叫潦。
打開BEC代幣合約源碼,找到batchTransfer這個方法:
這個函數的功能是:
調用該方法的人可以從自己的賬戶扣除相應的bec代幣官硝,給其他賬戶發(fā)送等額的代幣矗蕊。_receivers為需要發(fā)送bec的地址數組,_value表示每個地址發(fā)送多少bec氢架。
再來看該函數的邏輯:
uint cnt = _receivers.length;
首先取出接受地址個數傻咖,這筆交易發(fā)送給兩個地址,cnt為2达箍,這沒什么問題没龙。
uint256 amount = uint256(cnt) * _value;
然后算出總共需要消耗多少個代幣,看似也沒什么問題,但問題就出現在這里硬纤,我們繼續(xù)看后面的邏輯解滓。
require(cnt > 0 && cnt <= 20);
require(_value > 0 && balances[msg.sender] >= amount);
這里做了兩個判斷,主要看下面那個:發(fā)送者(sender)的代幣余額要大于等于剛剛算出來的amount才能夠繼續(xù)操作進行轉賬筝家,這樣的邏輯判斷也很合理洼裤,錢不夠銀行也不會給你轉賬。
但是問題來了:
如果在之前amount的乘法計算時溪王,amount溢出了為0腮鞍,那這個require(_value > 0 && balances[msg.sender] >= amount);判斷不就失效了。
黑客正是利用了這個漏洞莹菱,因為uint256表達的范圍是0到(2的256次方減1)移国,黑客只需要向兩個地址分別轉入(2的255次方)數量的代幣,最后合約計算出amount為2的256次方道伟,剛好溢出為0迹缀。導致balances[msg.sender] >= amount判斷失效。這樣黑客就可以憑空在自己的兩個賬戶產生大量的bec代幣蜜徽。
SMT合約漏洞
smt出現的合約漏洞也類似祝懂,也是通過整型溢出。
合約地址:https://etherscan.io/address/0x55f93985431fc9304077687a35a1ba103dc1e081#code
溢出攻擊交易:
https://etherscan.io/tx/0x1abab4c8db9a30e703114528e31dee129a3a758f7f8abc3b6494aad3d304e43f
可以看出如果feeSmt和_value相加的結果剛好為2的256次方拘鞋,出現整型溢出結果為0砚蓬,第206行的判斷將失效,讓攻擊者憑空產生代幣盆色。
如何防止整型溢出
使用SafeMath庫來進行算數運算灰蛙,在合約中添加代碼:
library SafeMath {
function mul(uint256 a, uint256 b) internal constant returns (uint256) {
if (a == 0) {
return 0;
}
uint256 c = a * b;
assert(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;
}
}
將BEC合約257行的乘法運算改為:
uint256 amount = uint256(cnt).mul(_value);
這樣在SafeMath執(zhí)行mul時,由于c計算出為0傅事,所以assert(c / a == b);將不通過缕允,拋出異常。
這里講一下require和assert的區(qū)別蹭越,assert會消耗執(zhí)行該函數的gas,而require只會消耗當前執(zhí)行的gas教届。