智能合约安全性分析 (智能合约安全)

零时科技 | 智能合约安全系列文章之反编译篇

前言

近年来,各个大型CTF(Capture The Flag,中文一般译作夺旗赛,在网络安全领域中指的是网络安全技术人员之间进行技术竞技的一种比赛形式)比赛中都有了区块链攻防的身影,而且基本都是区块链智能合约攻防。本次系列文章我们也以智能合约攻防为中心,来刨析智能合约攻防的要点,包括合约反编译,CTF常见题型及解题思路,相信会给读者带来不一样的收获。由于CTF比赛中的智能合约源代码没有开源,所以就需要从EVM编译后的opcode进行逆向来得到源代码逻辑,之后根据反编译后的源代码编写攻击合约,最终拿到flag。

基础

本篇我们主要来讲智能合约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和level
  • if (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.value
  • stop(); //stop表示该函数无返回值
  • return memory[temp7:temp7 + (temp6 + 0x20) - temp7]; //return表示该函数有返回值

总结

本篇主要分享的内容为,通过在线网站反编译智能合约opcode的一种方法,比较适合新手学习,下一篇我们会继续分享逆向智能合约的反汇编手法,希望对读者有所帮助。