基礎
本篇我們主要來講智慧合約opcode逆向,推薦的線上工具為Online Solidity Decompiler。該網站逆向的優點比較明顯,逆向後會得到合約反編譯的虛擬碼和反彙編的位元組碼,並且會列出合約的所有函式簽名(識別到的函式簽名會直接給出,未識別到的會給出UNknown),使用方式為下圖:
第一種方式是輸入智慧合約地址,並選擇所在網路
第二種方式是輸入智慧合約的opcode
逆向後的合約結果有兩個,一種是反編譯後的虛擬碼(偏向於邏輯程式碼,比較好理解),如下圖
另一種是反彙編後的位元組碼(需要學習位元組碼相關知識,不容易理解)。
本次演示使用的工具有:
Remix(線上編輯器):https://remix.ethereum.org/
Metamask(谷歌外掛):https://metamask.io/
Online Solidity Decompiler(逆向網站):https://ethervm.io/decompile/
案例一先來看一份簡單的合約反編譯,合約程式碼如下:
pragma solidity ^0.4.0; contract Data { uint De; function set(uint x) public { De = x; } function get() public constant returns (uint) { return De; } }
編譯後得到的opcode如下:
606060405260a18060106000396000f360606040526000357c01000000000000000000000000000000000000000000000000000000009004806360fe47b11460435780636d4ce63c14605d57603f565b6002565b34600257605b60048080359060200190919050506082565b005b34600257606c60048050506090565b6040518082815260200191505060405180910390f35b806000600050819055505b50565b60006000600050549050609e565b9056
利用線上逆向工具反編譯後(相關虛擬碼的含義已在程式碼段中詳細標註):
contract Contract { function main() { //分配記憶體空間 memory[0x40:0x60] = 0x60; //獲取data值 var var0 = msg.data[0x00:0x20] / 0x0100000000000000000000000000000000000000000000000000000000; //判斷呼叫是否和set函式簽名匹配,如果匹配,就繼續執行 if (var0 != 0x60fe47b1) { goto label_0032; } label_0043: //表示不接受msg.value if (msg.value) { label_0002: memory[0x40:0x60] = var0; //獲取data值 var0 = msg.data[0x00:0x20] / 0x0100000000000000000000000000000000000000000000000000000000; //判斷呼叫是否和set函式簽名匹配,如果匹配,就繼續執行 // Dispatch table entry for set(uint256) //這裡可得知set傳入的引數型別為uint256 if (var0 == 0x60fe47b1) { goto label_0043; } label_0032: //判斷呼叫是否和get函式簽名匹配,如果匹配,就繼續執行 if (var0 != 0x6d4ce63c) { goto label_0002; } //表示不接受msg.value if (msg.value) { goto label_0002; } var var1 = 0x6c; //這裡呼叫get函式 var1 = func_0090(); var temp0 = memory[0x40:0x60]; memory[temp0:temp0 + 0x20] = var1; var temp1 = memory[0x40:0x60]; //if語句後有return表示有返回值,前四行程式碼都是這裡的判斷條件,這裡返回值最終為var1 return memory[temp1:temp1 + (temp0 + 0x20) - temp1]; } else { var1 = 0x5b; //在這裡傳入的引數 var var2 = msg.data[0x04:0x24]; //呼叫get函式中var2引數 func_0082(var2); stop(); } } //下面定義了兩個函式,也就是網站列出的兩個函式簽名set和get //這裡函式傳入一個引數 function func_0082(var arg0) { //slot[0]=arg0 函式傳進來的引數 storage[0x00] = arg0; } //全域性變數標記: EVM將合約中的全域性變數存放在一個叫Storage的鍵值對虛擬空間, // 並且對不同的資料型別有對應的組織方法,存放方式為Storage[keccak256(add, 0x00)]。 // storage也可以理解成連續的陣列,稱為 `slot[]`,每個位置可以存放32位元組的資料 //函式未傳入引數,但有返回值 function func_0090() returns (var r0) { //這裡比較清楚,將上個函式傳入的引數slot[0]的值賦值給var0 var var0 = storage[0x00]; return var0; //最終返回 var0值 } }
透過上面的虛擬碼可以得到兩個函式set和get。set函式中,有明顯的傳參arg0,分析主函式main內容後,可得到該函式不接收以太幣,並且傳入的引數型別為uint256;get函式中,可明顯看出未傳入引數,但有返回值,也是不接收以太幣,透過storage[0x00]的相關呼叫可以得到返回值為set函式中傳入的引數。最終分析虛擬碼得到的原始碼如下:
contract AAA { uint256 storage; function set(uint256 a) { storage = a; } function get() returns (uint256 storage) { return storage; } }
相對而言,該合約反編譯後的虛擬碼比較簡單,只需要看反編譯後的兩個函式就可判斷出合約邏輯,不過對於邏輯函式較複雜的合約,反編譯後的虛擬碼就需要進一步判斷主函式main()中的內容。
案例二簡單入門之後,我們直接來分析一道CTF智慧合約的反編譯程式碼
合約地址:https://ropsten.etherscan.io/address/0x93466d15A8706264Aa70edBCb69B7e13394D049f#code
反編譯後得到的合約函式簽名及方法引數呼叫如下:
合約虛擬碼如下(相關虛擬碼的含義已在程式碼段中詳細標註,標註為重點):
contract Contract { function main() { memory[0x40:0x60] = 0x80; //判斷函式簽名是否為4位元組 // EVM裡對函式的呼叫都是取`bytes4(keccak256(函式名(引數型別1,引數型別2))`傳遞的,即對函式簽名做keccak256雜湊後取前4位元組 if (msg.data.length < 0x04) { revert(memory[0x00:0x00]); } //取函式簽名,前四個位元組(函式簽名四個位元組表示為0xffffffff型別) var var0 = msg.data[0x00:0x20] / 0x0100000000000000000000000000000000000000000000000000000000 & 0xffffffff; if (var0 == 0x2e1a7d4d) { // Dispatch table entry for withdraw(uint256) var var1 = msg.value; //表示不接受 `msg.value` if (var1) { revert(memory[0x00:0x00]); } var1 = 0x00be; var var2 = msg.data[0x04:0x24]; withdraw(var2); //stop表示該函式無返回值 stop(); } else if (var0 == 0x66d16cc3) { // Dispatch table entry for profit() var1 = msg.value; if (var1) { revert(memory[0x00:0x00]); } var1 = 0x00d5; profit(); stop(); } else if (var0 == 0x8c0320de) { // Dispatch table entry for payforflag(string,string) var1 = msg.value; if (var1) { revert(memory[0x00:0x00]); } var1 = 0x0184; var temp0 = msg.data[0x04:0x24] + 0x04; var temp1 = msg.data[temp0:temp0 + 0x20]; var temp2 = memory[0x40:0x60]; memory[0x40:0x60] = temp2 + (temp1 + 0x1f) / 0x20 * 0x20 + 0x20; memory[temp2:temp2 + 0x20] = temp1; memory[temp2 + 0x20:temp2 + 0x20 + temp1] = msg.data[temp0 + 0x20:temp0 + 0x20 + temp1]; var2 = temp2; var temp3 = msg.data[0x24:0x44] + 0x04; var temp4 = msg.data[temp3:temp3 + 0x20]; var temp5 = memory[0x40:0x60]; memory[0x40:0x60] = temp5 + (temp4 + 0x1f) / 0x20 * 0x20 + 0x20; memory[temp5:temp5 + 0x20] = temp4; memory[temp5 + 0x20:temp5 + 0x20 + temp4] = msg.data[temp3 + 0x20:temp3 + 0x20 + temp4]; var var3 = temp5; payforflag(var2, var3); stop(); } else if (var0 == 0x9189fec1) { // Dispatch table entry for guess(uint256) var1 = msg.value; if (var1) { revert(memory[0x00:0x00]); } var1 = 0x01b1; var2 = msg.data[0x04:0x24]; guess(var2); stop(); } else if (var0 == 0xa5e9585f) { // Dispatch table entry for xxx(uint256) var1 = msg.value; if (var1) { revert(memory[0x00:0x00]); } var1 = 0x01de; var2 = msg.data[0x04:0x24]; xxx(var2); stop(); } else if (var0 == 0xa9059cbb) { // Dispatch table entry for transfer(address,uint256) var1 = msg.value; if (var1) { revert(memory[0x00:0x00]); } var1 = 0x022b; var2 = msg.data[0x04:0x24] & 0xffffffffffffffffffffffffffffffffffffffff; var3 = msg.data[0x24:0x44]; transfer(var2, var3); stop(); } else if (var0 == 0xd41b6db6) { // Dispatch table entry for level(address) var1 = msg.value; if (var1) { revert(memory[0x00:0x00]); } var1 = 0x026e; var2 = msg.data[0x04:0x24] & 0xffffffffffffffffffffffffffffffffffffffff; var2 = level(var2); var temp6 = memory[0x40:0x60]; memory[temp6:temp6 + 0x20] = var2; var temp7 = memory[0x40:0x60]; //return表示該函式有返回值 return memory[temp7:temp7 + (temp6 + 0x20) - temp7]; } else if (var0 == 0xe3d670d7) { // Dispatch table entry for balance(address) var1 = msg.value; if (var1) { revert(memory[0x00:0x00]); } var1 = 0x02c5; var2 = msg.data[0x04:0x24] & 0xffffffffffffffffffffffffffffffffffffffff; var2 = balance(var2); var temp8 = memory[0x40:0x60]; memory[temp8:temp8 + 0x20] = var2; var temp9 = memory[0x40:0x60]; return memory[temp9:temp9 + (temp8 + 0x20) - temp9]; } else { revert(memory[0x00:0x00]); } } function withdraw(var arg0) { //在函式簽名處,已給出該函式傳參型別為uint256,判斷傳入的引數arg0是否等於2,如果為2,則繼續執行下面程式碼,否則退出 if (arg0 != 0x02) { revert(memory[0x00:0x00]); } memory[0x00:0x20] = msg.sender; //定義這個msg.sender的第一種型別,可透過balance函式判斷出,這裡為balance memory[0x20:0x40] = 0x00; //等同於require(arg0 <= balance[msg.sender]) if (arg0 > storage[keccak256(memory[0x00:0x40])]) { revert(memory[0x00:0x00]); } var temp0 = arg0; var temp1 = memory[0x40:0x60]; //將主要內容提取出來,可表示為address(msg.sender).call.gas(msg.gas).value(temp0 * 0x5af3107a4000) memory[temp1:temp1 + 0x00] = address(msg.sender).call.gas(msg.gas).value(temp0 * 0x5af3107a4000)(memory[temp1:temp1 + memory[0x40:0x60] - temp1]); memory[0x00:0x20] = msg.sender; memory[0x20:0x40] = 0x00; var temp2 = keccak256(memory[0x00:0x40]); //可寫為storage[temp2] -= temp0, 由之前程式碼可知temp0=arg0,由前一句的temp2 = keccak256(memory[0x00:0x40]);向上推理可得知這裡為msg.sender storage[temp2] = storage[temp2] - temp0; } function profit() { memory[0x00:0x20] = msg.sender; //定義這個msg.sender為第二種型別,可透過level函式判斷出,這裡為level memory[0x20:0x40] = 0x01; //這裡就等同於require(mapping2[msg.sender] == 0) if (storage[keccak256(memory[0x00:0x40])] != 0x00) { revert(memory[0x00:0x00]); } memory[0x00:0x20] = msg.sender; //啟用第一個型別balance進行後續運算 memory[0x20:0x40] = 0x00; var temp0 = keccak256(memory[0x00:0x40]); //這裡進行第一種型別balance的自加一,storage[arg0] += 1 storage[temp0] = storage[temp0] + 0x01; memory[0x00:0x20] = msg.sender; //啟用第二個型別level進行後續運算 memory[0x20:0x40] = 0x01; var temp1 = keccak256(memory[0x00:0x40]); //這裡進行第二種型別level的自加一,storage[0x80] += 1 storage[temp1] = storage[temp1] + 0x01; } //傳入兩個string型別的引數 function payforflag(var arg0, var arg1) { memory[0x00:0x20] = msg.sender; //啟用第一個型別balance進行後續運算 memory[0x20:0x40] = 0x00; //require(balance[msg.sender] >= 0x02540be400) if (storage[keccak256(memory[0x00:0x40])] < 0x02540be400) { revert(memory[0x00:0x00]); } memory[0x00:0x20] = msg.sender; //啟用第一個型別balance進行後續運算 memory[0x20:0x40] = 0x00; //將第一個型別balance賦值為0,等同於balance[msg.sender] = 0 storage[keccak256(memory[0x00:0x40])] = 0x00; var temp0 = address(address(this)).balance; var temp1 = memory[0x40:0x60]; var temp2; temp2, memory[temp1:temp1 + 0x00] = address(storage[0x02] & 0xffffffffffffffffffffffffffffffffffffffff).call.gas(!temp0 * 0x08fc).value(temp0)(memory[temp1:temp1 + memory[0x40:0x60] - temp1]); var var0 = !temp2; //傳入一個uint256型別的引數 function guess(var arg0) { if (arg0 != storage[0x03]) { revert(memory[0x00:0x00]); } //判斷傳入的引數是否和storage[0x03]值匹配, memory[0x00:0x20] = msg.sender; //啟用第二個型別level進行後續運算 memory[0x20:0x40] = 0x01; //判斷require(mapping1[msg.sender] == 1) if (storage[keccak256(memory[0x00:0x40])] != 0x01) { revert(memory[0x00:0x00]); } memory[0x00:0x20] = msg.sender; //啟用第一個型別balance進行後續運算 memory[0x20:0x40] = 0x00; var temp0 = keccak256(memory[0x00:0x40]); //這裡進行第一種型別balance的自加一,storage[0x80] += 1 storage[temp0] = storage[temp0] + 0x01; memory[0x00:0x20] = msg.sender; //啟用第二個型別level進行後續運算 memory[0x20:0x40] = 0x01; var temp1 = keccak256(memory[0x00:0x40]); //這裡進行第二種型別level的自加一,storage[0x80] += 1 storage[temp1] = storage[temp1] + 0x01; } function xxx(var arg0) { //storage[0x02] & 0xffffffffffffffffffffffffffffffffffffffff 表示storage[0x02]為一個地址型別 //判斷呼叫者發起人的地址是否為匹配 if (msg.sender != storage[0x02] & 0xffffffffffffffffffffffffffffffffffffffff) { revert(memory[0x00:0x00]); } //將傳入的uint256數值賦值給storage[0x03] storage[0x03] = arg0; } //傳入兩個引數分別為address和uint256 function transfer(var arg0, var arg1) { memory[0x00:0x20] = msg.sender; //啟用第一個型別balance進行後續運算 memory[0x20:0x40] = 0x00; //這裡為require(balance[msg.sender] >= arg1) if (storage[keccak256(memory[0x00:0x40])] < arg1) { revert(memory[0x00:0x00]); } //判斷arg1是否等於2,require(arg1 == 2) if (arg1 != 0x02) { revert(memory[0x00:0x00]); } memory[0x00:0x20] = msg.sender; //啟用第二個型別level進行後續運算 memory[0x20:0x40] = 0x01; if (storage[keccak256(memory[0x00:0x40])] != 0x02) { revert(memory[0x00:0x00]); } //判斷條件,為require(level[msg.sender] == 2) memory[0x00:0x20] = msg.sender; //啟用第一個型別balance進行後續運算 memory[0x20:0x40] = 0x00; //賦值操作:balance[msg.sender] = 0 storage[keccak256(memory[0x00:0x40])] = 0x00; memory[0x00:0x20] = arg0 & 0xffffffffffffffffffffffffffffffffffffffff; //啟用第一個型別balance進行後續運算 memory[0x20:0x40] = 0x00; //balance[address] = arg1 storage[keccak256(memory[0x00:0x40])] = arg1; } function level(var arg0) returns (var arg0) { memory[0x20:0x40] = 0x01; memory[0x00:0x20] = arg0; return storage[keccak256(memory[0x00:0x40])]; } function balance(var arg0) returns (var arg0) { memory[0x20:0x40] = 0x00; memory[0x00:0x20] = arg0; return storage[keccak256(memory[0x00:0x40])]; } }
透過分析上面經過詳細標註的反編譯虛擬碼,我們寫出合約原始碼:
contract babybank { address owner; uint secret; event sendflag(string base1,string base2); constructor()public{ owner = msg.sender; } function payforflag(string base1,string base2) public{ require(balance[msg.sender] >= 10000000000); balance[msg.sender]=0; owner.transfer(address(this).balance); emit sendflag(base1,base2); } modifier onlyOwner(){ require(msg.sender == owner); _; } function withdraw(uint256 amount) public { require(amount == 2); require(amount <= balance[msg.sender]); address(msg.sender).call.gas(msg.gas).value(amount * 0x5af3107a4000)(); balance[msg.sender] -= amount; } function profit() public { require(level[msg.sender] == 0); balance[msg.sender] += 1; level[msg.sender] += 1; } function xxx(uint256 number) public onlyOwner { secret = number; } function guess(uint256 number) public { require(number == secret); require(level[msg.sender] == 1); balance[msg.sender] += 1; level[msg.sender] += 1; } function transfer(address to, uint256 amount) public { require(balance[msg.sender] >= amount); require(amount == 2); require(level[msg.sender] == 2); balance[msg.sender] = 0; balance[to] = amount; } }
該反編譯合約中,需要判斷分析的點為合約中的邏輯函式和主函式main()的相關判斷。邏輯函式(withdraw,profit,payforflag,guess,xxx,transfer)中和主函式main()需要關注的點為:
memory[0x20:0x40] = 0x00和memory[0x20:0x40] = 0x01分別代表balance和levelif (arg1 != 0x02) { revert(memory[0x00:0x00]); }代表require(arg1 == 2),其他條件判斷與此相似if (msg.sender != storage[0x02] & 0xffffffffffffffffffffffffffffffffffffffff) { revert(memory[0x00:0x00]); } 表示為require(msg.sender == owner)storage[temp1] = storage[temp1] + 0x01;表示為level[msg.sender] += 1;if (msg.data.length < 0x04) { revert(memory[0x00:0x00]); } //判斷函式簽名是否為4位元組var var0 = msg.data[0x00:0x20] / 0x0100000000000000000000000000000000000000000000000000000000 & 0xffffffff; //取函式簽名,前四個位元組(函式簽名四個位元組表示為0xffffffff型別) ,EVM裡對函式的呼叫都是取bytes4(keccak256(函式名(引數型別1,引數型別2))傳遞的,即對函式簽名做keccak256雜湊後取前4位元組if (var1) { revert(memory[0x00:0x00]); } //表示不接受 msg.valuestop(); //stop表示該函式無返回值return memory[temp7:temp7 + (temp6 + 0x20) - temp7]; //return表示該函式有返回值總結