课程回顾 | Xrosheart: 以太坊智能合约漏洞介绍与规模化审计方法详解(中)

640


12月8日,看雪讲师Xrosheart在【传统安全×区块链】学习交流群为大家继续带来了课程 以太坊智能合约漏洞介绍与规模化审计方法详解(中)。接下来我们一起来回顾一下~


一、智能合约蜜罐介绍

 

<table><tr>

<td>

<img src=./image/ppt.2.png border=0 >

<div align="center">


640


</div>

</td></tr></table>

 

蜜罐就是一个陷阱、一个骗局,看似甜如蜜,实则危机四伏。智能合约蜜罐虽然不是设置来吸引黑客攻击的,但其就是一个骗局,利用甜如蜜的诱饵,欺骗受害者上当,从而获取利益。不同于一般“钓鱼”的对象之广泛,智能合约蜜罐主要面向的是智能合约开发者、智能合约代码审计人员或黑客,相对来说门槛较高,这就很契合蜜罐的含义了。



WhaleGiveaway1

 

它仅仅使用最原始的欺骗手法。虽然手法是拙劣的,但也有着一定的诱导性。其中文译为,超长空格的欺骗。

```js

if(msg.value>1 ether)

{

  msg._sender.transfer(this.balance);

}

`
``


这就是一个蜜罐,一个诱惑:只要转账金额大于 1 ether,就可以取走该智能合约里所有的以太币。

 

当然,这只是一个陷阱罢了,是我们的错觉而言,是由于代码编辑器显示超长时没有自动换行所致,我们仔细一看刚刚那段if语句,会发现源代码显示(也就是图5.2,见下图)的第19行,后面一长串的空格后面隐藏这一句转账语句,把这些空格去掉,这个if语句真实的面孔其实如下所示:

```js

if(msg.value>1 ether)

{

  Owner.transfer(this.balance);

  msg.sender.transfer(this.balance);

}

```

 

640


蜜罐诱惑背后的真面目是:如果转账金额大于 1 ether,就先将合约余额全部转给合约的Owner,然后再将剩余的余额(也就是0)转给转账的用户(也就是受害者)。


 

MultiplicatorX3

 

MultiplicatorX3说的通俗一点就是“合约永远比你有钱”的逻辑漏洞。为什么这么说呢?我们来看一下代码进行分析

 

我们可以在下面的网址找到该蜜罐合约的代码以及历史交易信息等:


https://etherscan.io/address/0x5aA88d2901C68fdA244f1D0584400368d2C8e739#code

 

下面展示来合约的代码,我们来进行分析:

```js

pragma solidity ^0.4.18;



contract MultiplicatorX3

{

  address public Owner = msg.sender;



  function() public payable{}



  function withdraw()

  payable

  public

   
{

      require(msg.sender == Owner);

      Owner.transfer(this.balance);

   }



  function Command(address adr,bytes data)

  payable

  public

   
{

      require(msg.sender == Owner);

      adr.call.value(msg.value)(data);

   }



  function multiplicate(address adr)

  public

  payable

   
{

      if(msg.value>=this.balance)

      {      

          adr.transfer(this.balance+msg.value);

      }

   }

}

```

 

合约的逻辑很简单,其三个关键函数的功能如下:

 

* withdraw( ):如果调用者地址等于合约Owner的地址,就获得合约的全部以太币;


* Command( ):如果调用者就是合约的Owner,就能向目标地址转发以太币并传入需要的data;


* multiplicate( ):只要转账的金额大于合约内余额,就可以把 **账户余额和本次转账的金额** 都转给一个可控的地址。

 

显然,这个蜜罐合约的关键代码就是函数```function multiplicate(address adr)```,其诱人之处在于刚才字面上的解释“**只要转账的金额大于合约内余额**,就可以把账户余额和本次转账的金额都转给一个可控的地址”,看起来条件很容易满足,而且还有丰厚的奖励——连本带利一起取回来。 

 

然而,真的如此吗?显然不是,这里的逻辑漏洞“合约永远比你有钱”确实比较隐蔽,容易被忽视。

 

我们来仔细分析一下调用函数 multiplicate 的过程:首先,账户要转入一定量的以太币,也就是msg.value;然后再进行函数内if语句的判断。 

 

以上标准的过程就意味着,在你调用函数的transaction被确认进入区块的同时,你转账的以太币msg.value就转入了合约,也就是说,这个步骤在if语句判断之前。此时就有 **合约余额 = 之前的合约余额 + 本次转账的金额**。那么下一步进行if语句判断的时候,合约余额当然大于本次转账的金额。无论转账金额多大,结果都是这样,这也就是所谓的“合约永远比你有钱”(实际上,**msg.value>= this.balance 只有在原余额为0,转账数量为0的时候才会成立**)。

 

那么被蜜罐吸引来的“攻击者”,调用函数 multiplicate 只会白白地将以太币转入合约却无法取出,反而成为来骗局的“受害者”。

 


TestBank

 

TestBank ,其逻辑漏洞简单来说就是“谁是合约主人”

 

我们可以在下面的网址找到该蜜罐合约的代码以及历史交易信息等内容:


https://etherscan.io/address/0x70C01853e4430cae353c9a7AE232a6a95f6CaFd9

 

下面我们给出其代码来进行分析。

```js

pragma solidity ^0.4.18;



contract Owned {

  address public owner;

  function Owned() { owner = msg.sender; }

  modifier onlyOwner{ if (msg.sender != owner) revert(); _; }

}



contract TestBank is Owned {

  event BankDeposit(address from, uint amount);

  event BankWithdrawal(address from, uint amount);

  address public owner = msg.sender;

  uint256 ecode;

  uint256 evalue;



  function() public payable {

      deposit();

   }



  function deposit() public payable {

      require(msg.value > 0);

      BankDeposit(msg.sender, msg.value);

   }



  function setEmergencyCode(uint256 code, uint256 value) public onlyOwner{

      ecode = code;

      evalue = value;

   }



  function useEmergencyCode(uint256 code) public payable {

      if ((code == ecode) && (msg.value == evalue)) owner =msg.sender;

   }



  function withdraw(uint amount) public onlyOwner {

      require(amount <=this.balance);

      msg.sender.transfer(amount);

   }

}

```

 

这个合约代码的蜜罐是什么呢?其实就是TestBank合约的 useEmergencyCode 函数。如果我们只看TestBank合约,只关注 useEmergencyCode 函数的代码,那其代码意思不就是说:如果我们可以通过 useEmergencyCode() 中的判断,即使得传入的code等于合约的ecode并且传入的金额value等于合约的evalue,那就可以将 owner 设置为我们的地址。

 

一旦我们将合约的owner设置为我们的地址,就可以通过 onlyOwner 这个函数修改器的判断,从而调用并执行 withdraw 函数取出合约内的以太币。这就是蜜罐。 

 

上面的分析是不是觉得很有道理?当然这都是假象,是由于对Solidity语言的继承功能的错误理解导致的。TestBank 合约继承自 Owned 合约,但是子类合约中```address public owner = msg.sender;```owner变量和父类合约中的重名了。那么在子类合约,也就是 TestBank 合约中使用的owner变量究竟是什么?难道像函数覆写一样,这个重名的变量也会覆写吗?答案是否定的。接下来我们来说明这点语法的误区。

 

我们来看一段示例代码进行说明。

```js

contract A{

  uint variable = 0;

  function test1(uint a) returns(uint){

     variable++;

     return variable;

   }

 function test2(uint a) returns(uint){

     variable += a;

     return variable;

   }

}

contract B is A{

  uint variable = 0;

  function test2(uint a) returns(uint){

      variable++;

      return variable;

   }

}

====================//分割线以下是上面代码等效的B合约

contract B{

  uint variable1 = 0;

  uint variable2 = 0;

  function test1(uint a) returns(uint v){

      variable1++;

     return variable1;

   }

  function test2(uint a) returns(uint v){

      variable2++;

      return variable2;

   }

}

```

 

首先我们要注意:**Solidity的继承原理是代码拷贝**,因此换句话说,继承的写法总是能够写成一个单独的合约。上面给出的示例代码中,分割线以下就是把分割线以上继承的写法写成独立的合约。而判断两个合约完全相同的原理是编译两个合约,比较它们的bytecode是否相同。(既有函数方法的哈希,也有参数的哈希。)

 

这里我们再明确一下,EVM如何定位一个hash值。当源码编译成了bytecode后,Solidity对每个public函数会有一个唯一标识的hash,这个hash值是通过函数名,参数类型来确定的(参数顺序有关)。函数的hash值与是否有返回值无关,与参数名字无关。因此对于EVM来说,重载的函数如果参数类型不一样,其实是两个完全不一样的函数。

 

在这个示例代码中,我们可以看到,B合约继承自A合约。其中,B合约有一个与A合约同名且参数完全一致的函数```functiontest2(uint a) returns(uint)```,但是函数内容不一样,这就是函数的覆写。而且子类未调用父类被重写的函数,也就是说完全一样的test2之间不存在调用关系,因此父类A中的test2被抛弃,只剩下B本身的test2函数。

 

另外,子类B和父类A有相同名字的变量,也就是variable。这里我们要强调一点:对EVM来说,每个storage变量都会有一个唯一标识的slot id。在这个例子来说,虽然变量名字都叫variable,但是从bytecode角度来看,他们是由不同的slot id来确定的,因此也和变量名字是什么没有关系。所以,实际上这里父类A和子类B中的两个同名的variable变量其实是互不相干的两个变量,就可以理解为一个叫variable1,一个叫variable2

 

那么对于B合约从A合约继承的test1函数来说,其调用的variable应当是指A合约中的variable,我们记做variable1;而对于B合约覆写的test2来说,其和A合约就没有关系了,调用的variable应当就是B合约新定义的variable,记做variable2。这样一来也就得到了分割线下面的等效的B合约。

 

示例代码分析加入理论讲了那么多,我们再回到这个蜜罐合约。

 

Owned合约本身有一个owner变量,而TestBank除了继承Owned合约外,自身又定义了一个owner变量。通过刚才示例代码的学习,我们知道,这两个变量其实是毫不相干的两个变量,我们就讲Owned合约的那个owner变量记做owner1,将后者记做owner2。除此以外,这段继承关系里面没有其他重名内容了,那么我们给出等效的B合约代码:

```js

contract TestBank is Owned {

  address public owner1 = msg.sender;

  modifier onlyOwner{ if (msg.sender != owner1) revert(); _; }



  event BankDeposit(address from, uint amount);

  event BankWithdrawal(address from, uint amount);

  address public owner2 = msg.sender;

  uint256 ecode;

  uint256 evalue;



  function() public payable {

      deposit();

   }



  function deposit() public payable {

      require(msg.value > 0);

      BankDeposit(msg.sender, msg.value);

   }



  function setEmergencyCode(uint256 code, uint256 value) public onlyOwner{

      ecode = code;

      evalue = value;

   }



  function useEmergencyCode(uint256 code) public payable {

      if ((code == ecode) && (msg.value == evalue)) owner2 = msg.sender;

   }



  function withdraw(uint amount) public onlyOwner {

      require(amount <= this.balance);

      msg.sender.transfer(amount);

   }

}

```

 

这个时候,我们擦亮眼睛看看仔细useEmergencyCode 函数只能修改owner2,而onlyOwner这个函数修改器判定的是是否是owner1。但owner1在一开始合约创建的时候就已经是创建者地址了,无法修改,也就意味着除了创建者owner1以外的账户都无法使用withdraw函数取出以太币。这就是蜜罐陷阱的真相。



 二、合约的解构


看下面这个简单的合约:

```js

pragma solidity ^0.4.24;



contract BasicToken {



uint256 totalSupply_;

mapping(address => uint256) balances;



constructor(uint256 _initialSupply) public {

  totalSupply_ = _initialSupply;

  balances[msg.sender] = _initialSupply;

 }



function totalSupply() public view returns (uint256) {

  return totalSupply_;

 }



function transfer(address _to, uint256 _value) public returns (bool) {

  require(_to != address(0));

  require(_value <= balances[msg.sender]);

  balances[msg.sender] = balances[msg.sender] - _value;

  balances[_to] = balances[_to] + _value;

  return true;

 }



function balanceOf(address _owner) public view returns (uint256) {

  return balances[_owner];

 }

}

```

 

该合约其实就是一个简单的代币合约 BasicToken。部署合约的时候其构造函数会初始化totalSupply_变量(用部署合约时账户传入的数据对其赋值),该变量也就是合约总共发行的代币数目,最初全部为合约部署者(也就是owner)所有。函数totalSupply()用于返回总共的代币数目。函数balanceOf(address)用于返回传入地址的代币数目。还有一个稍微复杂一些的函数transfer(address, uint256) 用于账户向某地址传输代币,其中涉及了一些代码稳固性的判断语句,不过在这里这些都不是重点。

 

接下来我们使用 remix 编译合约。点击文件浏览器区域上方左上角的 + 按钮,创建一个新的合约。将文件名设置为BasicToken.sol,然后将上面的代码粘贴到编辑器部分。

 

在右侧部分,转到 $Compile$ 选项卡,确保选中了$Enable$ $Optimization$,并验证所选的 $compiler$$version$ 是否为$version:0.4.24+commit.e67f0147.Emscripten.clang$。这两个细节非常重要,否则看到的字节码会与与这里讨论的稍有不同。

<table><tr>

<td>

<img src=./image/8.3.png border=0 >

<div align="center">

图8.3 remix界面

</div>

</td></tr></table>

 

另外,在$Compile$选项卡单击$Details$按钮,将看到一个弹出框,其中包含$Solidity$编译器生成的所有内容,其中之一是一个名为 BYTECODE JSON 对象,该对象具有一个“object”属性,即合约的编译代码。

<table><tr>

<td>

<img src=./image/8.4.png border=0 >

<div align="center">


640图8.4 BYTECODE


</div>

</td></tr></table>


这段编译代码长的是这样的:

"608060405234801561001057600080fd5b5060405160208061021783398101604090815290516000818155338152600160205291909120556101d1806100466000396000f3006080604052600436106100565763ffffffff7c010000000000000000000000000000000000000000000000000000000060003504166318160ddd811461005b57806370a0823114610082578063a9059cbb146100b0575b600080fd5b34801561006757600080fd5b506100706100f5565b60408051918252519081900360200190f35b34801561008e57600080fd5b5061007073ffffffffffffffffffffffffffffffffffffffff600435166100fb565b3480156100bc57600080fd5b506100e173ffffffffffffffffffffffffffffffffffffffff60043516602435610123565b604080519115158252519081900360200190f35b60005490565b73ffffffffffffffffffffffffffffffffffffffff1660009081526001602052604090205490565b600073ffffffffffffffffffffffffffffffffffffffff8316151561014757600080fd5b3360009081526001602052604090205482111561016357600080fd5b503360009081526001602081905260408083208054859003905573ffffffffffffffffffffffffffffffffffffffff85168352909120805483019055929150505600a165627a7a72305820a5d999f4459642872a29be93a490575d345e40fc91a7cccb2cf29c88bcdaf3be0029"


没错,这一串代码,对于一个普通人是无法读懂的,这就是我们的智能合约这部豪车引擎盖下的世界,Welcome!我们先搁置一下,一会再来分析这长串的字节码。

 

接下来,我们来部署合约。转到 $Run$ 选项卡上,选择了BasicToken和默认账户0xca3...a733c,并在部署输入框中输入数字10000。接下来,单击Deploy按钮,于是BasicToken合约部署成功,初始供应10000个令牌,这些令牌由我们之前选择的默认账户所拥有,它将保持我们的令牌供应的总量。

 

在Run选项卡的下面,在部署的合约部分,能看到部署的合约,包括与合约交互的三个函数:transfer、balanceOf、totalSupply 。再来看看合约“部署”的确切含义。在页面底部的控制台区域中,我们能看到日志“creation of BasicToken pending…”,然后是一个包含各种字段的transaction条目。单击此条目以展开transaction的信息。我们能看到日志记录的数据/输入与前面介绍的字节码相同。这些东西,在我们之前章节都有认识并使用。


(接下来transaction,我们通常叫做交易或事务)

 

在事务数据的右侧,仍然在控制台中,单击Debug按钮。这将激活Remix右侧区域中的调试器选项卡。让我们看一看说明书部分。如果你向下滚动,你会看到以下内容:

<table><tr>

<td>

<img src=./image/8.5.png border=0 >

<div align="center">

 

640

图8.5 OPCODE


</div>

</td></tr></table>

 

这是合约的分解字节码,即操作码OPCODE。实际上非常简单,如果按字节(一次扫描两个字符)扫描原始字节码,EVM将识别与特定操作相关联的特定操作码。例如:

```

0x60 => PUSH

0x01 => ADD

0x02 => MUL

0x00 => STOP

...

```

 

上面分解的代码中的每一行都是EVM执行的指令。每个指令包含一个操作码,操作码实际上就是结构字节码需要的一个基本的工具集,包括PUSH, ADD, SWAP, DUP等。例如,我们取其中一条指令,指令88,它将数字4推入堆栈。这个特定的反汇编程序解释指令如下:

```

88 PUSH1 0x04

| |     |    

| |     Hex value for push.

| Opcode.

Instruction number.

`
``

 

我们将在分解后的代码中识别分裂点并逐点减少它,直到我们最终得到小的、易于理解的块,我们将在Remix的调试器中一步一步地遍历这些块。在下面的图中,我们可以看到我们可以对分解的代码进行的第一次拆分,分成了creationruntime两部分,也就是合约部署时的代码和交互运行的代码。我们将在后面对其进行完整的分析。

<table><tr>

<td>

<img src=./image/8.6.png border=0 >

<div align="center">

 

640

图8.6 代码拆分图


</div>

</td></tr></table>

 

我们可以在解构图中找到整个解构的最终结果。如果一开始不理解图表,不要担心,接下来会一步步详细介绍,把图放在一边,这样就能在接下来的过程中了解全局。



互动


互动一:


。:蜜罐骗人是需要做宣传的吧


Xrosheart:不是啦,网站上都是能看到这些合约代码的,许多半吊子的合约开发者一看,觉得有利可图,就会转ether进去试图牟利。而这些交易实际上都是公开的,可以网站上看到,这些蜜罐骗到的人并不多,但确实有被骗到,毕竟面向群体的性质了。(https://etherscan.io/)



互动二:


阿杜:我有个疑问,这个蜜罐的外包装一般是什么?我的意思是,比如我收到某条中奖信息,我点开链接,我中毒了。但对于智能合约,比如这个蜜罐,我是做了什么操作会中招?


“糖果”(学生):就是你相信了他那个蜜罐逻辑,比如你以为发1ether就可以提取所有钱,你给他发钱不就触发了。


阿杜:蜜罐逻辑是给程序员看的,一般市民中招应该是通过大众页面访问的吧?比如卖东西你用亿台币结算等。那一般市民他是怎样访问到蜜罐?也是类似淘宝的购物页面?还是......


“糖果”(学生):大神说了就是针对开发者的额,见图(截图内容来自:讲师Xrosheart)


640


互动三:


“糖果”(学生):大神,我们年初的新书里有木有geth的源码分析。


Xrosheart:暂时没有呢,以后可以加上去哈。这个书主要是智能合约安全的。


“糖果”(学生):go语言好可怕~


Xrosheart:go语言蛮难入手的,但很多工程的东西用java和go比较多。


。:go可以生成跨平台的执行文件就很爽。


“糖果”(学生):光是看go就看的晕了......啊......心痛。啊,智能合约安全也好棒。


......


加入方式


扫描下方二维码,添加工作人员,由看雪工作人员邀你入群哦~640

640?


PS:看雪区块链官方QQ群:658766422,感兴趣的小伙伴们也可以进群交流哦!


640





640?

公众号ID:ikanxue

官方微博:看雪安全

商务合作:wsc@kanxue.com



  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值