以太坊智能合約逆向分析與實戰:(5)深入EVM之合約的部署與調用

當我們部署和調用合約的時候,EVM 都在做些什麼?

如果你開發過以太坊智能合約,想必你應該熟悉這樣的操作(此處以remix為例) :

編寫solidity代碼-> 編譯-> 部署-> 交互。合約的編寫與部署似乎並不是一件很麻煩的操作:編寫階段就不說了,Solidity語言大家都應該會;到了編譯階段,本地的solc 編譯器會把Solidity 代碼編譯成字節碼(bytecodes);而在部署階段,部署者通過發起一筆特殊交易(to的地址為空)calldata 帶上編譯後的字節碼,等交易上鍊之後,就完成了合約的部署;而合約交互,就是call合約裡的某個函數,等待函數的響應和返回,一切就是這樣的簡單。

但是正如開車一樣,當你踩住油門後,車輛開始前進。然而這看似簡單的操作背後是汽油爆燃、活塞往復、數百個齒輪嚙合傳動、輪胎與地面滾動摩擦的複雜行為。部署和調用合約也是如此,它涉及到EVM 的堆棧操作,內存讀寫,存儲訪問等一系列底層操作。當部署合約時, EVM 把收到的calldata 翻譯成操作指令,把它們按照給定的長度和參數讀入內存;當調用合約時,EVM 又根據收到的calldata ,通過函數選擇器來確定調用哪一段代碼,並返回數值。如果只講理論未免過於枯燥,為了便於講解,我們這次用ethernaut 的一道題目作為例子,詳細了解EVM 是如何部署和運行合約的,以及如何充當人肉編譯器,徒手編寫智能合約。

這個題目是這樣的:我們需要部署一個合約,當我們調用合約**whatIsTheMeaningOfLife()**函數的時候,它需要返回一個數字“42”。看起來很簡單對吧?我們分分鐘編寫完畢:

慢著,題目後面還有個小小的附加要求:“所部署的合約大小不超過10個操作碼”。好吧,這個要求的確夠“小”,要知道連合約頭部的“函數選擇器” 都不止10 個操作碼好吧?可是“函數選擇器” 是什麼,為什麼會出現在合約裡面呢?帶著你的疑問,繼續向下看。

我們通過./solc –asm –bin target.sol 來看看這個合約的最終編譯結果:

608060405234801561001057600080fd5b5060b68061001f6000396000f3fe6080604052348015600f57600080fd5b506004361060285760003560e01c8063650500c114602d575b600080fd5b60336047565b604051603e91906067565b60405180910390f35b6000602a905090565b6000819050919050565b6061816050565b82525050565b6000602082019050607a6000830184605a565b9291505056fea26469706673582212206ef8c7b5177952a701b3b46b69cb3ec296f4c54c946692e8ec901f5e43c1e78a64736f6c63430008110033

這麼一大坨十六進制數據,就是上述Solidity 程序編譯之後的字節碼。當我們部署合約時,把這一堆data 發給以太坊節點,等廣播完成後,合約就部署完畢了。這是solc 編譯器編譯Solidity程序得到的代碼,看似雜亂無章的的數據,其實都是和opcodes 一一對應的。我們來一段一段地看這些代碼:

合約部署代碼:

608060405234801561001057600080fd5b5060b68061001f6000396000f3fe

合約運行代碼:

6080604052348015600f57600080fd5b506004361060285760003560e01c8063650500c114602d575b600080fd5b60336047565b604051603e91906067565b60405180910390f35b6000602a905090565b6000819050919050565b6061816050565b82525050565b6000602082019050607a6000830184605a565b9291505056fe

auxdata:

a26469706673582212206ef8c7b5177952a701b3b46b69cb3ec296f4c54c946692e8ec901f5e43c1e78a64736f6c63430008110033

我們先簡單地把這堆代碼分為合約的部署代碼、運行代碼、auxdata 三部分,如何理解這三種代碼呢?我覺得可以理解為向太空發射衛星:“部署代碼” 就是運載火箭,而“運行代碼”就是衛星。運載火箭只在發射衛星時才起到作用,一旦衛星進入軌道,火箭就廢棄了,只留下衛星在太空中與地球通信。部署合約也是如此,在部署合約時,部署代碼把一些初始化工作作完之後,就把合約的運行代碼送入EVM,只留下運行代碼在鏈上與用戶進行交互。 (至於auxdata,它是緊跟在runtime代碼後面的43個字節,相當於源碼的指紋,可以用來驗證。這只是數據,並不會被EVM執行。)

那麼言歸正傳,我們題目要求我們合約運行代碼的opcedes 不超過10 條,那麼,這段代碼對應的opcodes 是多少條呢?答:71 條。 (通過查看Remix : ./artifacts/MagicNum.json 中的bytecode 裡的opcodes 可以看到。而deployedBytecode 裡的opcodes 卻是92 條,因為它的長度是部署代碼+ 運行代碼)

那麼問題來了,如何把71 條opcodes 精簡到10 條以內呢? 這就需要我們對EVM 運行智能合約的方式有著一定的了解。如果不了解也沒關係,拿起你手邊的EVM 指令集,我們一起來看看吧:

首先我們要知道,EVM 執行代碼時是按照自上而下的順序執行的,代碼中沒有其他入口點,始終從頂部(也就是第一行opcode ) 開始執行。 (這點和Windows 軟件不一樣,PE文件是有固定的入口點的,而且不同的Windows 版本或不同的PE 文件入口點也會有所不同)。也就是說,當我們部署合約時, EVM 會從第一個bytecode開始讀起。

所以我們看字節碼最前面的部分,也就是它的部署代碼:608060405234801561001057600080fd5b5060b68061001f6000396000f3fe

對照EVM 指令,我們可以識別出這段代碼的含義:

圖2

然後我們看合約的運行代碼:

6080604052348015600f57600080fd5b506004361060285760003560e01c8063650500c114602d575b600080fd5b60336047565b604051603e91906067565b60405180910390f35b6000602a905090565b6000819050919050565b6061816050565b82525050565b6000602082019050607a6000830184605a565b9291505056fe

圖3

圖4

綜合以上可以發現,合約的運行代碼的架構是這樣的:

圖5

初始化操作、函數選擇器這些,是solc 在編譯Solidity 程序的時候自動生成的。如果我們砍掉這些複雜的東西,直接把我們想要的核心功能編碼上去,不就可以在10 條以內opcodes 實現既定功能了嗎?

通過分析圖4 的whatIsTheMeaningOfLife() 函數調用棧可以得知,讓智能合約返回“42” ( 十六進制0x2a) 的關鍵在於先用mstore 指令將0x2a 放入Memory , 再用return 指令將內存裡的0x2a 返回即可。至於那些函數名稱和函數簽名,只是高級語言的編譯產物,直接用彙編實現的話,我們直接用這段代碼讀寫內存,完全沒有必要搞那些花里胡哨:

圖6

以上代碼相當於構造了一個十分小的合約“運行代碼”。前面我們說過,EVM 執行代碼時是按照自上而下的順序執行的,代碼中沒有其他入口點,始終從頂部(也就是第一行opcode ) 開始執行。而且我們編寫的代碼並沒有函數選擇器,也就是說,當外部賬戶調用該它時,無論傳遞給它什麼樣的參數、什麼樣的函數簽名, EVM 都只會從它的 [00] 處開始執行,老老實實地走到 [09],然後return 給我們一個0x20.

但這只是運行代碼,還記得本文開頭說的那三段字節碼嗎?是的,我們還差一個“運載火箭”(部署代碼),把這段運行代碼給發射出去:

部署代碼的結構基本沒怎麼變,之前已有解析,此處就不羅嗦了,唯一的區別是把複製到內存的長度由b6 改為0a : 608060405234801561001057600080fd5b50600a8061001f6000396000f3fe

然後把他們拼接到一起,記得部署代碼在前、運行代碼在後,最後我們把這段代碼發射出去就OK了:

你將得到一個超級小巧、只有10 個字節、無論傳遞什麼參數都只會返回 42 的“智能合約” (這麼說看起來並不智能的樣子……)

圖7

全文完。

關於作者:

來源:bress

Total
0
Shares
Related Posts