區塊鏈兄弟社區,區塊鏈技術專業問答先行者,中國區塊鏈技術愛好者聚集地
作者:吳壽鶴,《區塊鏈開發實戰——以太坊關鍵技術與案例分析》的第一作者,《區塊鏈開發實戰——Hyperledger Fabric關鍵技術與案例分析》聯合作者,IONChain 離子鏈 首席架構師,hyperLedger核心項目開發人員,區塊鏈技術社區-區塊鏈兄弟聯合創始人。github: https://github.com/gcc2ge
來源:區塊鏈兄弟,國內第一家專注區塊鏈技術分享實戰的公益性媒體社區
原文鏈接:http://www.blockchainbrother.com/article/1992
文章發布:請標題作者和來源,版權歸區塊鏈兄弟所有
事件回顧:
4月25日早間,火幣Pro公告,SMT項目方反饋今日凌晨發現其交易存在異常問題,經初步排查,SMT的以太坊智能合約存在漏洞。火幣Pro也同期檢測到TXID為https://etherscan.io/tx/0x0775e55c402281e8ff24cf37d6f2079bf2a768cf7254593287b5f8a0f621fb83的異常。受此影響,火幣Pro現決定暫停所有幣種的充提幣業務,隨后,火幣Pro又發布公告稱暫停SMT/USDT、SMT/BTC和SMT/ETH的交易。此外,OKEx,gate.io等交易平台也已經暫停了SMT的充提和交易。截止暫停交易,SMT在火幣Pro的價格下跌近20%。
該漏洞代理的直接經濟損失高達上億元人民幣,間接產生的負面影響目前無法估量。這到底是怎樣一個漏洞呢?下面將詳細分析該漏洞的產生和解決方案。
漏洞分析:
SMT與前幾天爆出的美圖BEC代幣都出現類似的安全漏洞—整數溢出,那么什么是整數溢出,整數溢出出現的原因是什么,怎樣才能避免整數溢出呢?接下來我們帶着這些問題來看下面的內容。
整數溢出
整數溢出分向上溢出和向下溢出,有關智能合約安全的其他關鍵點作者在《區塊鏈開發實戰——以太坊關鍵技術與案例分析》中有詳細介紹,以下是截取本書中關於整數溢出的部分,通過下面文字的閱讀大家就可以對:什么是整數溢出,整數溢出出現的原因是什么,怎樣才能避免整數溢出呢?這三個問題有個答案了。
以下文字截取於《區塊鏈開發實戰——以太坊關鍵技術與案例分析》
pragma solidity ^0.4.10;
/** 這是一個測試整數類型上溢和下溢的例子 */
contract Test{
// 整數上溢
//如果uint8 類型的變量達到了它的最大值(255),如果在加上一個大於0的值便會變成0
function test() returns(uint8){
uint8 a = 255;
uint8 b = 1;
return a+b;// return 0
}
//整數下溢
//如果uint8 類型的變量達到了它的最小值(0),如果在減去一個小於0的值便會變成255(uin8 類型的最大值)
function test_1() returns(uint8){
uint8 a = 0;
uint8 b = 1;
return a-b;// return 255
}
}
有了上面的理論基礎,我們在看一個轉賬的例子,看在我們的合約中應該如何避免不安全的代碼出現:
// 存儲用戶余額信息
mapping (address => uint256) public balanceOf;
// 不安全的代碼
// 函數功能:轉賬,這里沒有做整數溢出檢查
function transfer(address _to, uint256 _value) {
/* 檢查發送者是否有足夠的余額*/
if (balanceOf[msg.sender] < _value)
throw;
/* 修改發送者和接受者的余額 */
balanceOf[msg.sender] -= _value;
balanceOf[_to] += _value;
}
// 安全代碼
function transfer(address _to, uint256 _value) {
/* 檢查發送者是否有足夠的余額,同時做溢出檢查:balanceOf[_to] + _value < balanceOf[_to] */
if (balanceOf[msg.sender] < _value || balanceOf[_to] + _value < balanceOf[_to])
throw;
/* 修改發送者和接受者的余額 */
balanceOf[msg.sender] -= _value;
balanceOf[_to] += _value;
}
我們在做整數運算的時候要時刻注意上溢,下溢檢查,尤其對於較小數字的類型比如uint8、uint16、uint24更加要小心:它們更加容易達到最大值,最小值。
案例分析:
SMT合約中的整數安全問題簡析
SMT的合約地址是:0x55F93985431Fc9304077687a35A1BA103dC1e081,合約代碼可以訪問etherscan的如下網址進行查看
https://etherscan.io/address/0x55f93985431fc9304077687a35a1ba103dc1e081#code
SMT合約有問題的代碼存在於transferProxy()函數,代碼如下:
function transferProxy(address _from, address _to, uint256 _value, uint256 _feeSmt,
uint8 _v,bytes32 _r, bytes32 _s) public transferAllowed(_from) returns (bool){
if(balances[_from] < _feeSmt + _value) revert();
uint256 nonce = nonces[_from];
bytes32 h = keccak256(_from,_to,_value,_feeSmt,nonce);
if(_from != ecrecover(h,_v,_r,_s)) revert();
if(balances[_to] + _value < balances[_to]
|| balances[msg.sender] + _feeSmt < balances[msg.sender]) revert();
balances[_to] += _value;
Transfer(_from, _to, _value);
balances[msg.sender] += _feeSmt;
Transfer(_from, msg.sender, _feeSmt);
balances[_from] -= _value + _feeSmt;
nonces[_from] = nonce + 1;
return true;
}
其中的問題分析如下:
function transferProxy(address _from, address _to, uint256 _value, uint256 _feeSmt,
uint8 _v,bytes32 _r, bytes32 _s) public transferAllowed(_from) returns (bool){
//錯誤1:這里沒有做整數上溢出檢查
//_feeSmt,value都是由外部傳入的參數,通過我們之前的理論這里可能會出現整數上溢出的情況
// 例如:_feeSmt = 8fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff ,
// value = 7000000000000000000000000000000000000000000000000000000000000001
// _feeSmt和value均是uint256無符號整數,相加后最高位舍掉,結果為0。
// 那么_feeSmt + _value = 0 直接溢出,繞過代碼檢查,導致可以構造巨大數量的smt代幣並進行轉賬
if(balances[_from] < _feeSmt + _value) revert();
uint256 nonce = nonces[_from];
bytes32 h = keccak256(_from,_to,_value,_feeSmt,nonce);
if(_from != ecrecover(h,_v,_r,_s)) revert();
if(balances[_to] + _value < balances[_to]
|| balances[msg.sender] + _feeSmt < balances[msg.sender]) revert();
balances[_to] += _value;
Transfer(_from, _to, _value);
balances[msg.sender] += _feeSmt;
Transfer(_from, msg.sender, _feeSmt);
balances[_from] -= _value + _feeSmt;
nonces[_from] = nonce + 1;
return true;
}
作者修改后的代碼如下:
function transferProxy(address _from, address _to, uint256 _value, uint256 _feeSmt,
uint8 _v,bytes32 _r, bytes32 _s) public transferAllowed(_from) returns (bool){
//錯誤1:這里沒有做整數上溢出檢查
//_feeSmt,value都是由外部傳入的參數,通過我們之前的理論這里可能會出現整數上溢出的情況
// 例如:_feeSmt = 8fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff ,
// value = 7000000000000000000000000000000000000000000000000000000000000001
// _feeSmt和value均是uint256無符號整數,相加后最高位舍掉,結果為0。
// 那么_feeSmt + _value = 0 直接溢出,繞過代碼檢查,導致可以構造巨大數量的smt代幣並進行轉賬
// 在這里做整數上溢出檢查
if(balances[_to] + _value < balances[_to]
|| balances[msg.sender] + _feeSmt < balances[msg.sender]) revert();
// 在這里做整數上溢出檢查 ,防止交易費用 過大
if(_feeSmt + _value < _value ) revert();
// 在這里做整數上溢出檢查 ,防止交易費用 過大
if(balances[_from] < _feeSmt + _value) revert();
uint256 nonce = nonces[_from];
bytes32 h = keccak256(_from,_to,_value,_feeSmt,nonce);
if(_from != ecrecover(h,_v,_r,_s)) revert();
// 條件檢查盡量 在開頭做
// if(balances[_to] + _value < balances[_to]
// || balances[msg.sender] + _feeSmt < balances[msg.sender]) revert();
balances[_to] += _value;
Transfer(_from, _to, _value);
balances[msg.sender] += _feeSmt;
Transfer(_from, msg.sender, _feeSmt);
balances[_from] -= _value + _feeSmt;
nonces[_from] = nonce + 1;
return true;
}
攻擊者發送的交易:
以下是攻擊者惡意發送的轉賬交易地地址:https://etherscan.io/tx/0x1abab4c8db9a30e703114528e31dee129a3a758f7f8abc3b6494aad3d304e43f
(黑客攻擊交易日志截圖)
BEC合約中的整數安全問題簡析
BEC的合約地址是0xC5d105E63711398aF9bbff092d4B6769C82F793D,合約代碼可以訪問etherscan的如下網址進行查看https://etherscan.io/address/0xc5d105e63711398af9bbff092d4b6769c82f793d#code
BEC合約有問題的代碼存在於batchTransfer()函數,代碼如下:
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;
}
}
其中問題代碼分析如下:
function batchTransfer(address[] _receivers, uint256 _value) public whenNotPaused returns (bool) {
uint cnt = _receivers.length;
// 這里直接使用乘法運算符,可能會導致溢出
// 變量cnt為轉賬的地址數量,可以通過外界的用戶輸入_receivers進行控制,_value為單地址轉賬數額,也可以直接進行控制。
// 外界可以通過調整_receivers和_value的數值,產生乘法運算溢出,得出非預期amount數值,amount溢出后可以為一個很小的數字或者0,
uint256 amount = uint256(cnt) * _value;
require(cnt > 0 && cnt <= 20);
// 這里判斷當前用戶擁有的代幣余額是否大於或等於要轉移的amount數量
// 由於之前惡意用戶通過調大單地址轉賬數額_value的數值,使amount溢出后可以為一個很小的數字或者0,
// 所以很容易繞過balances[msg.sender] >= amount的檢查代碼。從而產生巨大_value數額的惡意轉賬。
require(_value > 0 && balances[msg.sender] >= amount);
//調用Safemath庫中的安全函數來完成加減操作
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;
}
}
攻擊者發送的交易:
以下是攻擊者惡意發送的轉賬交易:
https://etherscan.io/tx/0xad89ff16fd1ebe3a0a7cf4ed282302c06626c1af33221ebe0d3a470aba4a660f。
(黑客攻擊交易日志截圖)
合約整數漏洞事件總結
從上面的分析中,大家可以看出針對SMT和BEC的合約惡意攻擊都是通過惡意的整數溢出來繞過條件檢查。目前以太坊上運行着上千種合約,這上千種合約中可能也存在類似的安全隱患,所以作為合約的開發人員需要投入更多的精力來確保合約的安全性。
下篇我們將詳細的介紹如何正確保合約的安全,敬請期待。
文章發布只為分享區塊鏈技術內容,版權歸原作者所有,觀點僅代表作者本人,絕不代表區塊鏈兄弟贊同其觀點或證實其描述