以太坊中由Owner问题引发的CVE漏洞

一、构造函数引发问题

我们知道在大多数太坊合约均需要一个Owner来控制合约的正常运行,无论是涉及token的转账还是用于控制某些函数的启动。而许多合约中Owner的权限是非常大的,如果Owner的安全性得不到保障,那么作恶人员能够很轻易拿到合约的最高权限并轻易作恶,其带来的后果是非常严重的。

第一个问题也是最简单却是危害性最大的。

经常写合约代码的读者应该知道,早起的合约构造函数是使用合约名。例如:

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

当合约部署完毕后其构造函数自动执行,并赋值owner新的地址。而以太坊solidity在0.4.22后引入了新的构造函数声明形式constructor(),该函数引入的目的是避免编程人员在编写构造函数时的命名错误(这个问题我在前文也提到过,确实很伤-------)。然而,由于用户编写函数时习惯性的使用function进行声明,从而导致构造函数constructor的使用引入新的漏洞。

正确的构造函数形式:constructor() public { }

错误的构造函数形式:function constructor() public { }

在Solidity0.4.22版本后,合约的constructor()函数才被视为构造函数的形式,并且直到下一版本才会对function constructor()的形式给出警告(注意:这里仅仅是警告,不是错误)。如果是使用Solidity0.4.23之前的版本,编译器把function constructor()作为普通函数进行编译,认为是正确的普通函数。

下面我们来看一个例子:

在该例子中,编译器并没有因函数名不对而报错,反而一切正常。然而当我们仔细观察函数时我们就能发现上述的问题——即我们的构造函数多写了一个function。而这也会导致该函数无法起到构造函数的作用从而变成一个普通函数。

下面我们做一个简单的测试:

首先我们在本地部署合约:

当合约完成后调用Owner发现该owenr的address为0x000000。所以该函数根本没有起到作用。

而随着Solidity版本的更新,该问题也逐渐得到了解决,当我们使用最新版本的Solidity时,我们能发现:

此处使用的最新版本,它不通过编译。所以当现在合约编写者再编写合约的时候,应该多多考虑使用新版本,能够避免很多灾难性的失误。

1.新的constructor使用方法为,前面无function声明:

image.png

2.Remix-ide等编译器会对constructor的错误使用产生警告,开发者千万不要忽略编译器告警,推荐更改源码,消除所有编译器警告。

二、CVE-2018-11329

首先该漏洞展示的就是这个被人攻击的放置类游戏的智能合约,游戏名为Ether Cartel。与要求玩家孵化并且售卖虾子、之后提高产量再交易虾卵以换取以太币的的Ether Shrimp Farm应用类似,Ether Cartel也是相同的思路,只不过场景换成了某种非法物品交易。你所拥有的量越多,你生产的物品越多(一比一的生产比率)。收集更多的物品就可以加倍生产速度”。

image.png

在图中我们能够看出该合约误写了一个普通函数用于改变ceoAddress变量,而这个危害是非常大的。

image.png

当我更好账户的时候,我们可以任意调用DrugDealer()函数,并改变合约的owenr地址。

image.png

该漏洞为受ceoAnyone漏洞影响的放置游戏智能合约。Ether Cartel在UTC时间2018年5月18日17点14分56秒部署到主网,不幸的是一小时十一分钟之后即被攻破。存在漏洞的代码在18到20行:可被人以调用方调用的DrugDealer()函数,允许调用者修改收益地址——ceoAddress。

而后拿到了权限后,攻击者能够更进一步的调用其余函数,例如:

image.png

即当调用SellDrugs函数时会将fee转入ceoAddress账户,所以当该账户被恶意篡改后,该转账金额就会被窃取。同样下面的函数类似。

三、CVE-2018-10705

本章介绍CVE-2018-10705。该CVE同样是由于Owner没有进行严格把关从而产生的漏洞。审计该代码后发现,该合约中存在任意setowner函数,使得合约任意用户可以随意改变合约的owenr,并达到作恶的目的。

/**
 *Submitted for verification at Etherscan.io on 2018-01-17
*/

pragma solidity ^0.4.19;

interface tokenRecipient { function receiveApproval(address _from, uint256 _value, address _token, bytes _extraData) public; }

contract SafeMath {
  function safeMul(uint256 a, uint256 b) returns (uint256) {
    uint256 c = a * b;
    require(a == 0 || c / a == b);
    return c;
  }
  function safeSub(uint256 a, uint256 b) returns (uint256) {
    require(b <= a);
    return a - b;
  }
  function safeAdd(uint256 a, uint256 b) returns (uint256) {
    uint c = a + b;
    require(c >= a && c >= b);
    return c;
  }
}

contract Owned {
  address public owner;
  function Owned() {
    owner = msg.sender;
  }
  function setOwner(address _owner) returns (bool success) {
    owner = _owner;
    return true;
  }
  modifier onlyOwner {
    require(msg.sender == owner);
    _;
  }
}

contract AURA is SafeMath, Owned {
    bool public locked = true;
    string public name = "Aurora DAO";
    string public symbol = "AURA";
    uint8 public decimals = 18;
    uint256 public totalSupply;
    mapping (address => uint256) public balanceOf;
    mapping (address => mapping (address => uint256)) public allowance;

    event Transfer(address indexed from, address indexed to, uint256 value);

    /**
     * Constructor function
     *
     * Initializes contract with initial supply tokens to the creator of the contract
     */
    function AURA() public {
        totalSupply = 1000000000000000000000000000;
        balanceOf[msg.sender] = totalSupply;
    }

    /**
     * Internal transfer, only can be called by this contract
     */
    function _transfer(address _from, address _to, uint _value) internal {
        require(!locked || msg.sender == owner);
        require(_to != 0x0);
        require(balanceOf[_from] >= _value);
        require(balanceOf[_to] + _value > balanceOf[_to]);
        uint previousBalances = balanceOf[_from] + balanceOf[_to];
        balanceOf[_from] -= _value;
        balanceOf[_to] += _value;
        Transfer(_from, _to, _value);
        require(balanceOf[_from] + balanceOf[_to] == previousBalances);
    }

    /**
     * Transfer tokens
     *
     * Send `_value` tokens to `_to` from your account
     *
     * @param _to The address of the recipient
     * @param _value the amount to send
     */
    function transfer(address _to, uint256 _value) public returns (bool success) {
        _transfer(msg.sender, _to, _value);
        return true;
    }

    /**
     * Transfer tokens from other address
     *
     * Send `_value` tokens to `_to` in behalf of `_from`
     *
     * @param _from The address of the sender
     * @param _to The address of the recipient
     * @param _value the amount to send
     */
    function transferFrom(address _from, address _to, uint256 _value) public returns (bool success) {
        require(_value <= allowance[_from][msg.sender]);     // Check allowance
        allowance[_from][msg.sender] -= _value;
        _transfer(_from, _to, _value);
        return true;
    }

    /**
     * Set allowance for other address
     *
     * Allows `_spender` to spend no more than `_value` tokens in your behalf
     *
     * @param _spender The address authorized to spend
     * @param _value the max amount they can spend
     */
    function approve(address _spender, uint256 _value) public
        returns (bool success) {
        require(!locked);
        allowance[msg.sender][_spender] = _value;
        return true;
    }

    /**
     * Set allowance for other address and notify
     *
     * Allows `_spender` to spend no more than `_value` tokens in your behalf, and then ping the contract about it
     *
     * @param _spender The address authorized to spend
     * @param _value the max amount they can spend
     * @param _extraData some extra information to send to the approved contract
     */
    function approveAndCall(address _spender, uint256 _value, bytes _extraData)
        public
        returns (bool success) {
        tokenRecipient spender = tokenRecipient(_spender);
        if (approve(_spender, _value)) {
            spender.receiveApproval(msg.sender, _value, this, _extraData);
            return true;
        }
    }

    function unlockToken() onlyOwner {
      locked = false;
    }

    bool public balancesUploaded = false;
    function uploadBalances(address[] recipients, uint256[] balances) onlyOwner {
      require(!balancesUploaded);
      uint256 sum = 0;
      for (uint256 i = 0; i < recipients.length; i++) {
        balanceOf[recipients[i]] = safeAdd(balanceOf[recipients[i]], balances[i]);
        sum = safeAdd(sum, balances[i]);
      }
      balanceOf[owner] = safeSub(balanceOf[owner], sum);
    }
    
    function lockBalances() onlyOwner {
      balancesUploaded = true;
    }
}

首先,合约使用了安全函数,防止溢出(这一点也是值得我们注意的)。拿到Owned合约后,其构造函数首先初始化了owner。而后AURA合约继承了Owned合约。而该合约很明显与token有关系,里面存在许多ERC的函数:transfer、transferFrom、approve、approveAndCall

    function _transfer(address _from, address _to, uint _value) internal {
        require(!locked || msg.sender == owner);
        require(_to != 0x0);
        require(balanceOf[_from] >= _value);
        require(balanceOf[_to] + _value > balanceOf[_to]);
        uint previousBalances = balanceOf[_from] + balanceOf[_to];
        balanceOf[_from] -= _value;
        balanceOf[_to] += _value;
        Transfer(_from, _to, _value);
        require(balanceOf[_from] + balanceOf[_to] == previousBalances);
    }

而转账函数中出现了由owner控制的locked变量。该变量能控制函数是否进行。而该变量只能由下面的函数控制:

    function unlockToken() onlyOwner {
      locked = false;
    }

而我们回到父类合约中:


contract Owned {
  address public owner;
  function Owned() {
    owner = msg.sender;
  }
  function setOwner(address _owner) returns (bool success) {
    owner = _owner;
    return true;
  }
  modifier onlyOwner {
    require(msg.sender == owner);
    _;
  }
}

该合约却定义了一个修改owner的函数,即所有用户均可以修改owner???

这下麻烦事情来了。

首先部署合约。此时合约的owenr为:

image.png

切换用户后,我们执行unlockToken()

由于我们不是合约的owner,所以该函数无法执行成功。

之后我们调用setowner。

image.png

由于修改了owenr的信息,所以onlyOwner让我们绕过,我们可以恶意增加自己的余额。

image.png

除此之外,我们还能将调用

    function lockBalances() onlyOwner {
      balancesUploaded = true;
    }

来进行对函数的限制。

具体函数见:https://etherscan.io/address/0xcdcfc0f66c522fd086a1b725ea3c0eeb9f9e8814#contracts

四、参考

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

推荐阅读更多精彩内容