首頁>技術>
前言

透過上一篇反編譯文章的學習,我們對智慧合於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指令則表示並未有函式簽名匹配,從而停止執行,回滾狀態。

總結

由於反彙編內容過多,我們分為兩篇分享給大家,本篇我們對反彙編的內容進行了詳細講解,下篇我們將會繼續分析並串聯所有指令,梳理程式碼邏輯。

8
最新評論
  • BSA-TRITC(10mg/ml) TRITC-BSA 牛血清白蛋白改性標記羅丹明
  • 我是這樣跟面試官講垃圾回收的