透過上一篇反編譯文章的學習,我們對智慧合於opcode的反編譯有了基礎的學習,對於初學者來說,要想熟練運用還得多加練習。本篇我們來一塊學習智慧合約反彙編,同樣使用的是Online Solidity Decompiler線上網站,智慧合約反彙編對於初學者來說,較難理解,但對於智慧合約程式碼來說,只要能讀懂智慧合約反彙編,就可以非常清晰的瞭解到合約的程式碼邏輯,對審計合約和CTF智慧合約都有非常大的幫助
反彙編內容由於solidity智慧合約的opcode經過反彙編後,指令較多,我們本篇分析簡明要義,以一段簡單合約程式碼來分析其反彙編後的指令內容
合約原始碼如下:
pragma solidity ^0.4.24; contract Tee { uint256 private c; function a() public returns (uint256) { self(2); } function b() public { c++; } function self(uint n) internal returns (uint256) { if (n <= 1) { return 1; } return n * self(n - 1); } }
合約部署後生成的opcode:
0x6080604052600436106049576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680630dbe671f14604e5780634df7e3d0146076575b600080fd5b348015605957600080fd5b506060608a565b6040518082815260200191505060405180910390f35b348015608157600080fd5b5060886098565b005b60006094600260ab565b5090565b6000808154809291906001019190505550565b600060018211151560be576001905060cd565b60c86001830360ab565b820290505b9190505600a165627a7a7230582003f585ad588850fbfba4e8d96684e2c3fa427daf013d4a0f8e78188d4d475ee80029
透過線上網站Online Solidity Decompiler反彙編後結果(runtime bytecode)如下:
反彙編分析我們從第一部分指令label_0000開始
0000 60 PUSH1 0x80 0002 60 PUSH1 0x40 0004 52 MSTORE 0005 60 PUSH1 0x04 0007 36 CALLDATASIZE 0008 10 LT 0009 60 PUSH1 0x49 000B 57 *JUMPI
push指令是將位元組壓入棧頂,push1-push32依次代表將1位元組-32位元組推壓入棧頂,這裡PUSH1 0x80和PUSH1 0x40表示將0x80和0x40壓入棧頂,故目前棧的佈局如下:
1: 0x40 0: 0x80
MSTORE指令表示從棧中依次出棧兩個值arg0和arg1,並把arg1存放在記憶體的arg0處。目前來說棧中已無資料,這裡將0x80存放在記憶體0x40處。
PUSH1 0x04將0x04壓入棧中,CALLDATASIZE指令表示獲取msg.data呼叫資料,目前棧的佈局如下:
1: calldata 0: 0x04
LT指令表示將兩個棧頂的值取出,如果先出棧的值小於後出棧的值則把1入棧,反之把0入棧。這裡如果calldata呼叫資料小於0x04位元組,就將1入棧;如果calldata呼叫資料大於等於0x04位元組,就將0入棧。目前棧的佈局為:0: 0 或0: 1。
繼續分析,PUSH1 0x49指令將0x49壓入棧頂,目前棧的佈局為:
1:0x49 0: 0 或者 1
下面一條指令JUMPI指令表示從棧中依次出棧兩個值arg0和arg1,如果arg1的值為真則跳轉到arg0處,否則不跳轉。如果arg1值為1,則指令會跳轉到0x49處;如果arg1值為0,則會順序執行下一條指令。具體執行過程如下:
這裡我們先來分析順序執行的內容label_000C,指令如下
000C 60 PUSH1 0x00 000E 35 CALLDATALOAD 000F 7C PUSH29 0x0100000000000000000000000000000000000000000000000000000000 002D 90 SWAP1 002E 04 DIV 002F 63 PUSH4 0xffffffff 0034 16 AND 0035 80 DUP1 0036 63 PUSH4 0x0dbe671f 003B 14 EQ 003C 60 PUSH1 0x4e 003E 57 *JUMPI
目前經過上一步運算棧中佈局為空,PUSH1 0x00指令將0壓入棧中。CALLDATALOAD指令接受一個引數,該引數可以作為發往智慧合約的calldata資料的索引,然後從該索引處再讀取32位元組數,由於前一個指令傳入的索引值為0,所以這一步指令會彈出棧中的0,將calldata32位元組壓入棧中。PUSH29指令將29個位元組壓入棧中。目前棧的佈局如下:
1:0x0100000000000000000000000000000000000000000000000000000000 0:calldata值
SWAP1指令表示將堆疊頂部元素與之後的第一個元素進行交換,也就是0x0100000000000000000000000000000000000000000000000000000000和calldata值進行交換。接下來DIV指令表示(棧中第一個元素 // 棧中第二個元素)取a//b的值,這裡也就是calldata的32位元組除29位元組,由於除法的運算關係,這裡進行除法運算後的位元組為4位,估計大家也可以想到,這就是函式識別符號4位元組。那麼目前棧的佈局如下:
0:函式識別符號4位元組
PUSH4 指令將0xffffffff壓入棧中。AND指令表示將取棧中前兩個引數進行AND運算,也就是函式識別符號前四位0xffffffff進行AND操作,最終得到前四位的函式識別符號及後28位為空補0的數值。下一條指令DUP1表示複製當前棧中第一個值到棧頂,目前棧中佈局如下:
1:呼叫引數中的函式識別符號 0:呼叫引數中的函式識別符號
下一個指令PUSH4指令繼續將函式識別符號0x0dbe671f壓入棧中,這裡的識別符號為a()函式,函式識別符號我們可以在https://www.4byte.directory/線上網站檢視。目前棧中佈局如下:
2:0x0dbe671f 1:呼叫引數中的函式識別符號 0:呼叫引數中的函式識別符號
EQ指令表示取兩個棧頂值,如果兩值相等就將1入棧(也就是說a()函式識別符號與呼叫引數中的函式識別符號相等),反之將0入棧。下一步PUSH1將0x4e壓入棧頂。之後JUMPI指令從棧中依次出棧兩個值arg0和arg1,如果arg1的值為真則跳轉到arg0處,否則不跳轉。目前棧中佈局如下:
2:0x4e 1:1 或 0 0:呼叫引數中的函式識別符號
從前面三個指令可看出,EQ對函式識別符號進行判斷後,下一步壓入0x4e是為了JUMPI進行判斷並跳轉。也就是說如果EQ判斷a()函式識別符號相等(將1入棧),JUMPI執行後就會跳轉到0x4e的偏移位置;反之如果EQ判斷a()函式識別符號不相等(將0入棧),JUMPI執行後就會順序執行下一條語句。目前棧中佈局如下:
0:呼叫引數中的函式識別符號
具體執行過程如下:
目前我們對label_0000和label_000C已進行分析,從上圖來看,該流程中除了順序執行外,label_0000處0x49,label_003F處0x76和label_000C處0x4e都有相應的跳轉條件。本篇我們繼續分析順序執行部分(label_003F和label_0049)指令。首先來看第一部分label_003F:
003F 80 DUP1 0040 63 PUSH4 0x4df7e3d0 0045 14 EQ 0046 60 PUSH1 0x76 0048 57 *JUMPI
由於目前棧中只有一條資料(0:呼叫引數中的函式識別符號)
DUP1指令表示複製棧中第一個值到棧頂。PUSH4指令將0x4df7e3d0函式識別符號壓入棧頂,這裡函式識別符號代表b()函式,故目前棧中佈局如下:
2:0x4df7e3d0 1:呼叫引數中的函式識別符號 0:呼叫引數中的函式識別符號
接下來三個指令會進行棧中值進行運算和偏移量跳轉設定,EQ指令把棧頂的兩個值出棧,如果0x4df7e3d0和呼叫引數中的函式識別符號相等則把1入棧,否則把0入棧。PUSH1指令將偏移量0x76壓入棧中。JUMPI指令從棧中依次出棧兩個值:0x76和EQ指令判斷的值(1或0),如果EQ指令判斷的值為真則跳轉到0x76處,否則按順序執行不跳轉。故目前棧中佈局如下:
2:0x76 1:1 或 0 0:呼叫引數中的函式識別符號
我們假設EQ指令判斷的值為0,那麼透過JUMPI指令條件判斷後,會按照順序繼續執行下一條指令。執行後,棧中依然只有一條指令(0:呼叫引數中的函式識別符號)。
我們繼續進行順序執行,label_0049:
0049 5B JUMPDEST 004A 60 PUSH1 0x00 004C 80 DUP1 004D FD *REVERT
JUMPDEST指令在該上下文中表示跳轉回來,也就是label_0000處0x49的跳轉。之後的兩條指令PUSH1和DUP1總體意思為將0壓入棧頂並複製,沒有實際意義。REVERT指令則表示並未有函式簽名匹配,從而停止執行,回滾狀態。
總結由於反彙編內容過多,我們分為兩篇分享給大家,本篇我們對反彙編的內容進行了詳細講解,下篇我們將會繼續分析並串聯所有指令,梳理程式碼邏輯。