時間:2018年9月6日 系列文章:
一、簡介
在知道創宇404區塊鏈安全研究團隊整理輸出的《知道創宇以太坊合約審計CheckList》中,把“溢出問題”、“重入漏洞”、“權限控制錯誤”、“重放攻擊”等問題統一歸類爲“以太坊智能合約編碼安全問題”。
“昊天塔(HaoTian)”是知道創宇404區塊鏈安全研究團隊獨立開發的用於監控、掃描、分析、審計區塊鏈智能合約安全自動化平臺。我們利用該平臺針對上述提到的《知道創宇以太坊合約審計CheckList》中“以太坊智能合約編碼安全”類問題在全網公開的智能合約代碼做了掃描分析。詳見下文:
二、漏洞詳情
1、溢出問題
以太坊Solidity設計之初就被定位爲圖靈完備性語言。在solidity的設計中,支持int/uint變長的有符號或無符號整型。變量支持的步長以8遞增,支持從uint8到uint256,以及int8到int256。需要注意的是,uint和int默認代表的是uint256和int256。uint8的數值範圍與C中的uchar相同,即取值範圍是0到2^8-1,uint256支持的取值範圍是0到2^256-1。而當對應變量值超出這個範圍時,就會溢出至符號位,導致變量值發生巨大的變化。
(1) 算數溢出
在Solidity智能合約代碼中,在餘額的檢查中如果直接使用了加減乘除沒做額外的判斷時,就會存在算術溢出隱患
contract MyToken {
mapping (address => uint) balances;
function balanceOf(address _user) returns (uint) { return balances[_user]; }
function deposit() payable { balances[msg.sender] += msg.value; }
function withdraw(uint _amount) {
require(balances[msg.sender] - _amount > 0); // 存在整數溢出
msg.sender.transfer(_amount);
balances[msg.sender] -= _amount;
}
}
在上述代碼中,由於沒有校驗
_amount
一定會小於
balances[msg.sender]
,所以攻擊者可以通過傳入超大數字導致溢出繞過判斷,這樣就可以一口氣轉走鉅額代幣。
2018年4月24日,SMT/BEC合約被惡意攻擊者轉走了50,659,039,041,325,800,000,000,000,000,000,000,000,000,000,000,000,000,000,000個SMT代幣。惡意攻擊者就是利用了 SMT/BEC合約的整數溢出漏洞 導致了這樣的結果。
2018年5月19日,以太坊Hexagon合約代幣被公開存在 整數溢出漏洞 。
(2) 鑄幣燒幣溢出問題
作爲一個合約代幣的智能合約來說,除了有其他合約的功能以外,還需要有鑄幣和燒幣功能。而更特殊的是,這兩個函數一般都爲乘法或者指數交易,很容易造成溢出問題。
function TokenERC20(
uint256 initialSupply,
string tokenName,
string tokenSymbol
) public {
totalSupply = initialSupply * 10 ** uint256(decimals);
balanceOf[msg.sender] = totalSupply;
name = tokenName;
symbol = tokenSymbol;
}
上述代碼未對代幣總額做限制,會導致指數算數上溢。
2018年6月21日,Seebug Paper公開了一篇關於整數溢出漏洞的分析文章 ERC20 智能合約整數溢出系列漏洞披露 ,裏面提到很多關於指數上溢的漏洞樣例。
2、call注入
Solidity作爲一種用於編寫以太坊智能合約的圖靈完備的語言,除了常見語言特性以外,還提供了調用/繼承其他合約的功能。在
call
、
delegatecall
、
callcode
三個函數來實現合約之間相互調用及交互。正是因爲這些靈活各種調用,也導致了這些函數被合約開發者“濫用”,甚至“肆無忌憚”提供任意調用“功能”,導致了各種安全漏洞及風險。
function withdraw(uint _amount) {
require(balances[msg.sender] >= _amount);
msg.sender.call.value(_amount)();
balances[msg.sender] -= _amount;
}
上述代碼就是一個典型的存在call注入問題直接導致重入漏洞的demo。
2016年7月, The DAO 被攻擊者使用重入漏洞取走了所有代幣,損失超過60億,直接導致了eth的硬分叉,影響深遠。
2017年7月20日,Parity Multisig電子錢包版本1.5+的漏洞被發現,使得攻擊者從三個高安全的多重簽名合約中竊取到超過15萬ETH ,其事件原因是由於未做限制的 delegatecall 函數調用了合約初始化函數導致合約擁有者被修改。
2018年6月16日,「隱形人真忙」在先知大會上分享了 「智能合約消息調用攻防」 的議題,其中提到了一種新的攻擊場景——call注⼊,主要介紹了利用對call調用處理不當,配合一定的應用場景的一種攻擊手段。接着於 2018年6月20日,ATN代幣團隊發佈「ATN抵禦黑客攻擊的報告」,報告指出黑客利用call注入攻擊漏洞修改合約擁有者,然後給自己發行代幣,從而造成 ATN 代幣增發。
2018年6月26日,知道創宇區塊鏈安全研究團隊在Seebug Paper上公開了 《以太坊 Solidity 合約 call 函數簇濫用導致的安全風險》 。
3、權限控制錯誤
在智能合約中,合約開發者一般都會設置一些用於合約所有者,但如果開發者疏忽寫錯了函數權限,就有可能導致所有者轉移等嚴重後果。
function initContract() public {
owner = msg.reader;
}
上述代碼函數就需要設置onlyOwner。
4、重放攻擊
2018年,DEFCON26上來自 360 獨角獸安全團隊(UnicornTeam)的 Zhenzuan Bai, Yuwei Zheng 等分享了議題 《Your May Have Paid More than You Imagine:Replay Attacks on Ethereum Smart Contracts》
在攻擊中提出了智能合約中比較特殊的委託概念。
在資產管理體系中,常有委託管理的情況,委託人將資產給受託人管理,委託人支付一定的費用給受託人。這個業務場景在智能合約中也比較普遍。
這裏舉例子爲transferProxy函數,該函數用於當user1轉token給user3,但沒有eth來支付gasprice,所以委託user2代理支付,通過調用transferProxy來完成。
function transferProxy(address _from, address _to, uint256 _value, uint256 _fee,
uint8 _v, bytes32 _r, bytes32 _s) public returns (bool){
if(balances[_from] < _fee + _value
|| _fee > _fee + _value) revert();
uint256 nonce = nonces[_from];
bytes32 h = keccak256(_from,_to,_value,_fee,nonce,address(this));
if(_from != ecrecover(h,_v,_r,_s)) revert();
if(balances[_to] + _value < balances[_to]
|| balances[msg.sender] + _fee < balances[msg.sender]) revert();
balances[_to] += _value;
emit Transfer(_from, _to, _value);
balances[msg.sender] += _fee;
emit Transfer(_from, msg.sender, _fee);
balances[_from] -= _value + _fee;
nonces[_from] = nonce + 1;
return true;
}
上述代碼nonce值可以被預測,而其他變量不變的情況下,可以通過重放攻擊來多次轉賬。
三、漏洞影響範圍
使用Haotian平臺智能合約審計功能可以準確掃描到該類型問題。
基於Haotian平臺智能合約審計功能規則,我們對全網的公開的共42538個合約代碼進行了掃描,其中共1852個合約涉及到這類問題。
1、溢出問題
截止2018年9月5日,我們發現了391個存在算數溢出問題的合約代碼,其中332個仍處於交易狀態,其中交易量最高的10個合約情況如下:
截止2018年9月5日,我們發現了1636個存在超額鑄幣銷幣問題的合約代碼,其中1364個仍處於交易狀態,其中交易量最高的10個合約情況如下:
2、call注入
截止2018年9月5日,我們發現了204個存在call注入問題的合約代碼,其中140個仍處於交易狀態,其中交易量最高的10個合約情況如下:
3、重放攻擊
截止2018年9月5日,我們發現了18個存在重放攻擊隱患問題的合約代碼,其中16個仍處於交易狀態,其中交易量最高的10個合約情況如下:
四、修復方式
1、溢出問題
1) 算術溢出問題
在調用加減乘除時,通常的修復方式都是使用openzeppelin-safeMath,但也可以通過對不同變量的判斷來限制,但很難對乘法和指數做什麼限制。
function transfer(address _to, uint256 _amount) public returns (bool success) {
require(_to != address(0));
require(_amount <= balances[msg.sender]);
balances[msg.sender] = balances[msg.sender].sub(_amount);
balances[_to] = balances[_to].add(_amount);
emit Transfer(msg.sender, _to, _amount);
return true;
}
2)鑄幣燒幣溢出問題
鑄幣函數中,應對totalSupply設置上限,避免因爲算術溢出等漏洞導致惡意鑄幣增發。
在鑄幣燒幣加上合理的權限限制可以有效減少該問題危害。
contract OPL {
// Public variables
string public name;
string public symbol;
uint8 public decimals = 18; // 18 decimals
bool public adminVer = false;
address public owner;
uint256 public totalSupply;
function OPL() public {
totalSupply = 210000000 * 10 ** uint256(decimals);
...
}
2、call注入
call函數調用時,應該做嚴格的權限控制,或直接寫死call調用的函數。避免call函數可以被用戶控制。
在可能存在重入漏洞的代碼中,經可能使用transfer函數完成轉賬,或者限制call執行的gas,都可以有效的減少該問題的危害。
contract EtherStore {
// initialise the mutex
bool reEntrancyMutex = false;
uint256 public withdrawalLimit = 1 ether;
mapping(address => uint256) public lastWithdrawTime;
mapping(address => uint256) public balances;
function depositFunds() public payable {
balances[msg.sender] += msg.value;
}
function withdrawFunds (uint256 _weiToWithdraw) public {
require(!reEntrancyMutex);
require(balances[msg.sender] >= _weiToWithdraw);
// limit the withdrawal
require(_weiToWithdraw <= withdrawalLimit);
// limit the time allowed to withdraw
require(now >= lastWithdrawTime[msg.sender] + 1 weeks);
balances[msg.sender] -= _weiToWithdraw;
lastWithdrawTime[msg.sender] = now;
// set the reEntrancy mutex before the external call
reEntrancyMutex = true;
msg.sender.transfer(_weiToWithdraw);
// release the mutex after the external call
reEntrancyMutex = false;
}
}
上述代碼是一種用互斥鎖來避免遞歸防護方式。
3、權限控制錯誤
合約中不同函數應設置合理的權限
檢查合約中各函數是否正確使用了public、private等關鍵詞進行可見性修飾,檢查合約是否正確定義並使用了modifier對關鍵函數進行訪問限制,避免越權導致的問題。
function initContract() public OnlyOwner {
owner = msg.reader;
}
4、重放攻擊
合約中如果涉及委託管理的需求,應注意驗證的不可複用性,避免重放攻擊。
其中主要的兩點在於: 1、避免使用transferProxy函數。採用更靠譜的簽名方式簽名。 2、nonce機制其自增可預測與這種簽名方式違背,導致可以被預測。儘量避免nonce自增。
五、一些思考
在完善智能合約審計checklist時,我選取了一部分問題將其歸爲編碼安全問題,這類安全問題往往是開發者疏忽導致合約代碼出現漏洞,攻擊者利用代碼中的漏洞來攻擊,往往會導致嚴重的盜幣事件。
在我們使用HaoTian對全網的公開合約進行掃描和監控時,我們發現文章中提到的幾個問題涉及到的合約較少。由於智能合約代碼公開透明的特性,加上這類問題比較容易檢查出,一旦出現就會導致對合約的毀滅性打擊,所以大部分合約開發人員都會注意到這類問題。但在不容易被人們發現的未公開合約中,或許還有大批潛在的問題存在。
這裏我們建議所有的開發者重新審視自己的合約代碼,檢查是否存在編碼安全問題,避免不必要的麻煩或嚴重的安全問題。
智能合約審計服務
針對目前主流的以太坊應用,知道創宇提供專業權威的智能合約審計服務,規避因合約安全問題導致的財產損失,爲各類以太坊應用安全保駕護航。
知道創宇404智能合約安全審計團隊:
https://www.scanv.com/lca/index.html
聯繫電話:(086) 136 8133 5016(沈經理,工作日:10:00-18:00)
歡迎掃碼諮詢: 區塊鏈行業安全解決方案
黑客通過DDoS攻擊、CC攻擊、系統漏洞、代碼漏洞、業務流程漏洞、API-Key漏洞等進行攻擊和入侵,給區塊鏈項目的管理運營團隊及用戶造成巨大的經濟損失。知道創宇十餘年安全經驗,憑藉多重防護+雲端大數據技術,爲區塊鏈應用提供專屬安全解決方案。
歡迎掃碼諮詢:
本文由 Seebug Paper 發佈,如需轉載請註明來源。本文地址: https://paper.seebug.org/696/