深入解析Solidity合約

yaohsin
Taipei Ethereum Meetup
10 min readJan 4, 2019

這次主要想討論Solidity背後的運作原理,到底一個Solidity合約經過編譯到部署上鏈之間的過程是如何運作的,以及後續呼叫合約時的流程,知道得越多就越能寫出安全可靠的合約。

本文是參考OpenZeppelin的系列文章『Deconstructing a Solidity Contract,內容相當精彩,建議可以跟著演練一次。

我們都知道一個Solidity程式碼寫完之後,必須將它編譯成byte code,才能透過交易部署至鏈上,變成所謂的智能合約。我們以下面的EtherDice.sol為例子。

EtherDice v1

當EtherDice.sol經過編譯之後,會產出下面的一長串byte code,這些可以透過EVM assbemly來一步步解析,但用肉眼很難看出到底是如何運作的,至少我不行。

608060405267016345785d8a00003411151561001a57600080fd5b610332806100296000396000f3fe608060405260043610610051576000357c010000000000000000000000000000000000000000000000000000000090048063026b1d5f146100565780637365870b14610081578063cd5e3c5d146100c7575b600080fd5b34801561006257600080fd5b5061006b6100f2565b6040518082815260200191505060405180910390f35b6100ad6004803603602081101561009757600080fd5b8101908080359060200190929190505050610111565b604051808215151515815260200191505060405180910390f35b3480156100d357600080fd5b506100dc6102f2565b6040518082815260200191505060405180910390f35b60003073ffffffffffffffffffffffffffffffffffffffff1631905090565b6000808210158015610124575060058211155b1515610198576040517f08c379a00000000000000000000000000000000000000000000000000000000081526004018080602001828103825260198152602001807f5f6e756d626572206973206265747765656e203020746f20350000000000000081525060200191505060405180910390fd5b60006101a26102f2565b905080831415610273573373ffffffffffffffffffffffffffffffffffffffff166108fc600234029081150290604051600060405180830381858888f193505050501580156101f5573d6000803e3d6000fd5b507f19d7eac3f3db644d690a8b91f7e31894dbc050a6dbf885c516ccb3dc99ef3241838233604051808481526020018381526020018273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001935050505060405180910390a160019150506102ed565b7f19d7eac3f3db644d690a8b91f7e31894dbc050a6dbf885c516ccb3dc99ef324183826000604051808481526020018381526020018273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001935050505060405180910390a160009150505b919050565b600060064281151561030057fe5b0690509056fea165627a7a723058207764b48131629493d8b87c2587e91375b785aefcae2d01c52340f658ab33a8600029

以執行的方式來看,這些bytecode可以先分成固定的三個部分,如下:

  • Creation — 這部分是用來執行合約裡的constructor,而且不會儲存在區塊內,這也就是為什麼constructor只會執行一次。可以看到下圖綠色的部分,constructor執行完畢之後,就是取出Runtime部分,最後將它儲存在區塊中。
  • Runtime — 這部分是合約的主體,除了constructor之外的部分,鏈上儲存的就是這份bytecode。在部署合約的時候,並不會執行Runtime的部分,而是在後續跟合約互動的時候才會用到。
  • Constructor Parameters — constructor的參數通通放在最後面,方便用完捨棄。
合約編譯後的byte code的圖示

合約部署上鏈之後,在使用合約內的某個函式時,其流程又是如何運作的?這就歸Runtime部分的byte code來負責了。如下圖,Runtime可以分成幾個固定的部分,其中最常用到的就屬 function selector ,它主要負責逐一比對想要呼叫的function signature是否有存在byte code中,若有就執行,否則就會執行fallback function。若合約連fallback function都沒實作,則交易就會被Revert。

Runtime部分

簡單介紹完合約部署上鏈及呼叫函式的流程之後,讓我們來看看對於EtherDice合約的攻防思路,就會變得相當容易理解。

1. 小明的發財夢

小明是個區塊鏈的初學者,想趁著這股風潮趁機大撈一筆。他心裡盤算著,大家滿手ether沒地方花,不如來開發個賭博遊戲吧。這遊戲必須符合容易上手、無法精通的重要原則,才能吸引大量玩家。好吧,就來開發經典的骰子遊戲,容易理解又可以靈活的控制勝率,根本是穩賺不賠的生意。
測試版沒兩三下就開發好了,小明是個認真用功的好學生,記得老師上課時講過,要先在測試鏈上面跑跑看,沒問題再放到主鏈上,才是正確的開發流程。於是小明照辦了,就先放到Ropsten Testnet吧!因為是個dApp,所以程式碼當然也是要公開透明,讓玩家知道莊家沒有一點作弊的可能。為求保險,他還將程式碼上放某個知名的bug bounty網站 — solidified,公開懸賞漏洞。一切都設置好之後,終於可以安心睡覺了。

2. EtherDice的漏洞

阿財是個業餘的bug hunter,平時最大的嗜好就是挖bug賺獎金。又是個沒局的夜晚,阿財跟平常一樣獨自坐在電腦前看文章,信箱突然登登一聲,一封主旨為New bounty posted的email出現在螢幕的角落,阿財馬上點開信中的連結,想要搶快挖bug。經驗豐富的阿財只看EtherDice的合約名稱就馬上知道可能的漏洞在哪邊,果不其然,又是一個想發財的新手,這下撿到寶了。

function roll() public view returns(uint) {        
return (block.timestamp % 6);
}

骰子遊戲中最重要的random number來源居然是使用區塊的timestamp,只要寫個攻擊的合約,透過合約來呼叫EtherDice的bet,就可以事先計算block.timestamp % 6的結果再來下注,就每賭必贏啦!!阿財很迅速的完成攻擊合約,並寫下建議的修補方式,就提交bug了。

攻擊合約

3. 修補EtherDice的漏洞

小明才剛躺在床上都還沒睡著,手機就傳來email的通知,沒想到居然這麼快就被找到bug,經過測試確認之後,DiceHack.sol的確是可以成功壓中每一筆下注。他一邊喃喃自語solidified不愧是全球最大的solidity bug bounty網站,效率真高,這錢花的實在值得,一邊著手修補漏洞。

防禦的概念是這樣的,只要能判斷發出交易的address是一般的 externally owned account (EOA,也就是由使用者控制密鑰的帳戶),還是 contract account,就可以擋下由DiceHack.sol發出的交易,這樣就沒問題啦。

合約部署上鏈後,會將runtime部分儲存起來,透過判斷某個address是否擁有runtime的byte code就可知它是否為contract address。幸好,solidity支援inline assembly,可以直接在solidity程式碼內使用 extcodesize取出儲存的byte code長度,若長度為0就表示是EOA發出的,否則就是合約。

assembly { size := extcodesize(addr) }

小明修改完程式碼之後,再跑一次DiceHack.sol,果然成功擋下,看到螢幕上跳出 robot is not allowed!! 訊息,嘴角不禁微微抽動。這下終於可以高枕無憂,離他的發財夢也不遠了。

EtherDice v2

4. 道高一尺 魔高一丈

一切都在阿財的預料之中,但令他感到意外的是這個新手居然馬上就修補好漏洞,這樣的熱血青年實在令人讚賞,沈思幾秒之後,阿財心裡默默說出,好吧,這次就放過你了。於是著手提交第二個bug,並附上他早就預備好的第二支攻擊合約。

攻擊思路是這樣的,solidity經過編譯之後,主要有creation及runtime兩個部分的byte code,首先執行creation,此時,runtime程式碼尚未儲存,這樣extcodesize(addr) 的回傳值一定會是0。因此只要把攻擊程式碼寫在合約的constructor裡面,就可以繞過偵測機制,發動攻擊。

攻擊合約 v2

這次阿財並沒有說明防禦的方法,因為他希望這位熱血青年能夠繼續鑽研技術,用自己的方式找出答案。其實他這麼做都是為了增加取暖人數,畢竟幣圈正值寒冬,能多一人是一人。

曾經有人說過:若寫程式碼時還要加一堆註解,那不如去寫小說。

所以我來寫小說了XDDD。

--

--