智慧合約安全系列文章反彙編·上篇

智慧合約安全系列文章反彙編·上篇

前言

透過上一篇反編譯文章的學習,我們對智慧合於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  push2 0x80
  0002    60  push2 0x40
  0004    52  mstore
  0005    60  push2 0x04
  0007    36  calldatasize
  0008    10  lt
  0009    60  push2 0x49
  000b    57 *jumpi

push指令是將位元組壓入棧頂,push2-push32依次代表將1位元組-32位元組推壓入棧頂,這裡push2 0x80和push2 0x40表示將0x80和0x40壓入棧頂,故目前棧的佈局如下:

 1: 0x40
 0: 0x80

mstore指令表示從棧中依次出棧兩個值arg0和arg1,並把arg1存放在記憶體的arg0處。目前來說棧中已無資料,這裡將0x80存放在記憶體0x40處。

push2 0x04將0x04壓入棧中,calldatasize指令表示獲取msg.data呼叫資料,目前棧的佈局如下:

 1: calldata
 0: 0x04

lt指令表示將兩個棧頂的值取出,如果先出棧的值小於後出棧的值則把1入棧,反之把0入棧。這裡如果calldata呼叫資料小於0x04位元組,就將1入棧;如果calldata呼叫資料大於等於0x04位元組,就將0入棧。目前棧的佈局為:0: 0 或0: 1。

繼續分析,push2 0x49指令將0x49壓入棧頂,目前棧的佈局為:

 1:0x49
 0: 0 或者 1

下面一條指令jumpi指令表示從棧中依次出棧兩個值arg0和arg1,如果arg1的值為真則跳轉到arg0處,否則不跳轉。如果arg1值為1,則指令會跳轉到0x49處;如果arg1值為0,則會順序執行下一條指令。具體執行過程如下:

這裡我們先來分析順序執行的內容label_000c,指令如下

     000c    60  push2 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  push2 0x4e
  003e    57 *jumpi

目前經過上一步運算棧中佈局為空,push2 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入棧。下一步push2將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  push2 0x76
	0048    57  *jumpi

由於目前棧中只有一條資料(0:呼叫引數中的函式識別符號)

dup1指令表示複製棧中第一個值到棧頂。push4指令將0x4df7e3d0函式識別符號壓入棧頂,這裡函式識別符號代表b()函式,故目前棧中佈局如下:

2:0x4df7e3d0
1:呼叫引數中的函式識別符號
0:呼叫引數中的函式識別符號

接下來三個指令會進行棧中值進行運算和偏移量跳轉設定,eq指令把棧頂的兩個值出棧,如果0x4df7e3d0和呼叫引數中的函式識別符號相等則把1入棧,否則把0入棧。push2指令將偏移量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  push2 0x00
	004c    80  dup1
	004d    fd  *revert

jumpdest指令在該上下文中表示跳轉回來,也就是label_0000處0x49的跳轉。之後的兩條指令push2和dup1總體意思為將0壓入棧頂並複製,沒有實際意義。revert指令則表示並未有函式簽名匹配,從而停止執行,回滾狀態。

總結

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



免責聲明:

  1. 本文版權歸原作者所有,僅代表作者本人觀點,不代表鏈報觀點或立場。
  2. 如發現文章、圖片等侵權行爲,侵權責任將由作者本人承擔。
  3. 鏈報僅提供相關項目信息,不構成任何投資建議

推荐阅读