暫停交易?ERC20合約整數溢出安全漏洞案例技術分析(一)


區塊鏈兄弟社區,區塊鏈技術專業問答先行者,中國區塊鏈技術愛好者聚集地

作者:吳壽鶴,《區塊鏈開發實戰——以太坊關鍵技術與案例分析》的第一作者,《區塊鏈開發實戰——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的合約惡意攻擊都是通過惡意的整數溢出來繞過條件檢查。目前以太坊上運行着上千種合約,這上千種合約中可能也存在類似的安全隱患,所以作為合約的開發人員需要投入更多的精力來確保合約的安全性。

下篇我們將詳細的介紹如何正確保合約的安全,敬請期待。

文章發布只為分享區塊鏈技術內容,版權歸原作者所有,觀點僅代表作者本人,絕不代表區塊鏈兄弟贊同其觀點或證實其描述


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM