智慧合約安全系列文章反彙編·上篇
前言
透過上一篇反編譯文章的學習,我們對智慧合於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指令則表示並未有函式簽名匹配,從而停止執行,回滾狀態。
總結
由於反彙編內容過多,我們分為兩篇分享給大家,本篇我們對反彙編的內容進行了詳細講解,下篇我們將會繼續分析並串聯所有指令,梳理程式碼邏輯。