首頁>技術>

基礎

本篇我們主要來講智慧合約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表示該函式有返回值

總結

10
最新評論
  • BSA-TRITC(10mg/ml) TRITC-BSA 牛血清白蛋白改性標記羅丹明
  • 微控制器常用宏定義|列舉|自定義型別用法技巧