区块链安全—UCN代码漏洞详解

一、前言

很久没有对以太坊DAPP进行研究分析了,今天审计代码时偶然发现一款很有趣的DAPP应用。在以往的DAPP研究中,我们总能发现吸引用户投资的地方,例如前几篇文章中我们曾经介绍过Fomo3D游戏合约,为了吸引用户参与参考了心理学、经济学等理论。它让用户为了最后的大奖而进行疯狂进行投注行为,从而使游戏永远无法停止。而对于庞氏代币合约来说,它吞并了用户的本金并使新用户加入来实现盈利。

而在这次的分析中,我们通过其参与者的官方网站并没有发现其激励机制。也就是说在我看来就是一个很无聊的游戏而已emmm。但是还是吸引了很多玩家进行参与,并将钱均转向某个特定钱包地址。而在代码分析过程中,我还发现了除了DAPP本身之外的代码漏洞。这里一并写在这里。

二、DAPP—SaveUnicoins应用功能介绍

读者可以访问http://www.saveunicoins.com/#来查看该DAPP的官网。

image.png

该应用使用了与独角兽(unicorn)相近的英语单词(unicoins)来作为噱头进行应用设计。而查看游戏介绍我们发现其内容很笼统。大致意思如下:

image.png

SAVEUNICOINS是一款建立在以太坊上的DAPP应用,参与者需要“充币”来喂养这个独角兽。然而介绍中说道这里面的Unicoin可以通过官网或者智能合约来换取以太币,不过经过我分析代码后,并没有发现相关的转账代码(这个我稍后进行代码分析)。所以这个DAPP游戏就是令用户不断充钱,然后用什么来激励用户呢?

image.png

合约介绍中说明到,我们做这款DAPP是让用户来通过换取的token来喂养我们的独角兽,收到喂养的独角兽就可以不断的长大。而作为奖励,投食的用户可以获得全网广播的特权,也就是用户可以发一段消息,而这个消息将作为区块链的数据使所有用户均能够看到。对于区块链来说,由于数据是全网区块,所有用户均可以看到。

image.png

上图为游戏界面,我们可以通过这两种操作来购买Token代币并对独角兽进行投食。

三、代码详解

本次代码量并不是很大,所以在分析中我将把合约的各个模块内容进行分析。之后我们将对漏洞情况进行分析。所以本文不仅针对于了解合约漏洞用户,还针对分析代币合约的读者。希望能帮助读者更多的了解以太坊DAPP的搭建核心内容。

我们来具体的看代码内容。

首先我们能够看到创建者定义了一个ERC20Basic合约。

contract ERC20Basic {
  uint256 public totalSupply=100000000; 
  function balanceOf(address who) constant returns (uint256);
  function transfer(address to, uint256 value);
  event Transfer(address indexed from, address indexed to, uint256 value);
}

在该合约中,我们对balanceOftransfer进行了定义,并且设定了Transfer的事件,以方便对后面的合约内容进行展开。而这里也设定了totalSupply的总数为100000000。这也是一个基本的ERC20的代币合约的编程规则。

之后,代码对ERC20Basic进行扩展,实习了ERC20合约。

contract ERC20 is ERC20Basic {
  function allowance(address owner, address spender) constant returns (uint256);
  function transferFrom(address from, address to, uint256 value);
  function approve(address spender, uint256 value);
  event Approval(address indexed owner, address indexed spender, uint256 value);
}

而该合约继承了ERC20Basic并扩展出了另外三种赋权功能的函数,
为ERC20代币注入新活力。

而在上面的合约中,相关函数只是起到了声明的作用,所以下面的合约对函数进行了补充。

contract BasicToken is ERC20Basic {
  using SafeMath for uint256;

  mapping(address => uint256) balances;

  /**
  * @dev transfer token for a specified address
  * @param _to The address to transfer to.
  * @param _value The amount to be transferred.
  */
  function transfer(address _to, uint256 _value) {
    balances[msg.sender] = balances[msg.sender].sub(_value);
    balances[_to] = balances[_to].add(_value);
    Transfer(msg.sender, _to, _value);
  }


  

  /**
  * @dev Gets the balance of the specified address.
  * @param _owner The address to query the the balance of. 
  * @return An uint256 representing the amount owned by the passed address.
  */
  function balanceOf(address _owner) constant returns (uint256 balance) {
    return balances[_owner];
  }

}

这里使用了安全函数,并重写了transfer转账函数与余额查看函数,这些属于基本操作,难度不大。

而后,一个完整的标准代币合约就诞生了。

contract StandardToken is ERC20, BasicToken {

  mapping (address => mapping (address => uint256)) allowed;


  /**
   * @dev Transfer tokens from one address to another
   * @param _from address The address which you want to send tokens from
   * @param _to address The address which you want to transfer to
   * @param _value uint256 the amout of tokens to be transfered
   */
  function transferFrom(address _from, address _to, uint256 _value) {
    var _allowance = allowed[_from][msg.sender];

    // Check is not needed because sub(_allowance, _value) will already throw if this condition is not met
    // if (_value > _allowance) throw;

    balances[_to] = balances[_to].add(_value);
    balances[_from] = balances[_from].sub(_value);
    allowed[_from][msg.sender] = _allowance.sub(_value);
    Transfer(_from, _to, _value);
  }

  /**
   * @dev Aprove the passed address to spend the specified amount of tokens on behalf of msg.sender.
   * @param _spender The address which will spend the funds.
   * @param _value The amount of tokens to be spent.
   */
  function approve(address _spender, uint256 _value) {

    // To change the approve amount you first have to reduce the addresses`
    //  allowance to zero by calling `approve(_spender, 0)` if it is not
    //  already 0 to mitigate the race condition described here:
    //  https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
    if ((_value != 0) && (allowed[msg.sender][_spender] != 0)) throw;

    allowed[msg.sender][_spender] = _value;
    Approval(msg.sender, _spender, _value);
  }

  /**
   * @dev Function to check the amount of tokens that an owner allowed to a spender.
   * @param _owner address The address which owns the funds.
   * @param _spender address The address which will spend the funds.
   * @return A uint256 specifing the amount of tokens still avaible for the spender.
   */
  function allowance(address _owner, address _spender) constant returns (uint256 remaining) {
    return allowed[_owner][_spender];
  }

}

StandardToken包括了ERC20代币的所有功能,包括转账、赋权转账、赋权操作等等。而上面的内容均是ERC20代币合约的基础部分,后面的代码就是针对此应用而特定进行设计的。

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

     // This contract only defines a modifier but does not use
     // it - it will be used in derived contracts.
     // The function body is inserted where the special symbol
     // "_;" in the definition of a modifier appears.
     // This means that if the owner calls this function, the
     // function is executed and otherwise, an exception is
     // thrown.
     modifier onlyOwner {
         if(msg.sender != owner)
         {
         throw;
         }
         _;
     }
 }

owned合约中定义了一个修饰器。使用了此修饰器的函数只允许onlyOwner进行操作。

下面是UniContract代币系统的关键代码。

首先是变量的定义。

   string public constant name = "SaveUNICOINs";
   string public constant symbol = "UCN";
   uint256 public constant decimals = 0;
   
   //founder & fund collector
   address public multisig;
   address public founder; 
   
   
   //Timings
   uint public start;  
   uint public end;
   uint public launch;
   
   //Dynamic Pricing PRICE IN UCN
   uint256 public PRICE = 300000;  
   
   //Dynamic Status of sold UCN Tokens
   uint256 public OVERALLSOLD = 0;  
   
   //Maximum of Tokens to be sold 85.000.000
   uint256 public MAXTOKENSOLD = 85000000;  

其中,multisig与founder为地址变量,在后续的操作中将用户的value均转入到这两个地址中来。之后设计了三个关于时间的变量:start end launch。而这三个参数对应了三个时间节点。之后的两个OVERALLSOLD 以及MAXTOKENSOLD分别定义了目前出售的token数量以及设定的系统最大token数量。

下面是构造函数:

   function UniContract() onlyOwner { 
       founder = 0x204244062B04089b6Ef55981Ad82119cEBf54F88; 
       multisig= 0x9FA2d2231FE8ac207831B376aa4aE35671619960; 
       start = 1507543200;
       end = 1509098400; 
       launch = 1509534000;
       balances[founder] = balances[founder].add(15000000); // Founder (15% = 15.000.000 UCN)
   }

正如我们看到的,构造函数中设定了上述的变量,并且给founder用户初始化了15000000的代币。

//Stage Pre-Sale Variables
   
   uint256 public constant PRICE_PRESALE = 300000;  
   uint256 public constant FACTOR_PRESALE = 38;
   uint256 public constant RANGESTART_PRESALE = 0; 
   uint256 public constant RANGEEND_PRESALE = 10000000; 
   
   
   //Stage 1
   uint256 public constant PRICE_1 = 30000;  
   uint256 public constant FACTOR_1 = 460;
   uint256 public constant RANGESTART_1 = 10000001; 
   uint256 public constant RANGEEND_1 = 10100000;
   
   //Stage 2
   uint256 public constant PRICE_2 = 29783;  
   uint256 public constant FACTOR_2 = 495;
   uint256 public constant RANGESTART_2 = 10100001; 
   uint256 public constant RANGEEND_2 = 11000000;
   
   //Stage 3
   uint256 public constant PRICE_3 = 27964;  
   uint256 public constant FACTOR_3 = 580;
   uint256 public constant RANGESTART_3 = 11000001; 
   uint256 public constant RANGEEND_3 = 15000000;
   
   //Stage 4
   uint256 public constant PRICE_4 = 21068;  
   uint256 public constant FACTOR_4 = 800;
   uint256 public constant RANGESTART_4 = 15000001; 
   uint256 public constant RANGEEND_4 = 20000000;
   
   //Stage 5
   uint256 public constant PRICE_5 = 14818;  
   uint256 public constant FACTOR_5 = 1332;
   uint256 public constant RANGESTART_5 = 20000001; 
   uint256 public constant RANGEEND_5 = 30000000;
   
   //Stage 6
   uint256 public constant PRICE_6 = 7310;  
   uint256 public constant FACTOR_6 = 2700;
   uint256 public constant RANGESTART_6 = 30000001; 
   uint256 public constant RANGEEND_6 = 40000000;
   
   //Stage 7
   uint256 public constant PRICE_7 = 3607;  
   uint256 public constant FACTOR_7 = 5450;
   uint256 public constant RANGESTART_7 = 40000001; 
   uint256 public constant RANGEEND_7 = 50000000;
   
   //Stage 8
   uint256 public constant PRICE_8 = 1772;  
   uint256 public constant FACTOR_8 = 11000;
   uint256 public constant RANGESTART_8 = 50000001; 
   uint256 public constant RANGEEND_8 = 60000000;
   
   //Stage 9
   uint256 public constant PRICE_9 = 863;  
   uint256 public constant FACTOR_9 = 23200;
   uint256 public constant RANGESTART_9 = 60000001; 
   uint256 public constant RANGEEND_9 = 70000000;
   
   //Stage 10
   uint256 public constant PRICE_10 = 432;  
   uint256 public constant FACTOR_10 = 46000;
   uint256 public constant RANGESTART_10 = 70000001; 
   uint256 public constant RANGEEND_10 = 80000000;
   
   //Stage 11
   uint256 public constant PRICE_11 = 214;  
   uint256 public constant FACTOR_11 = 78000;
   uint256 public constant RANGESTART_11 = 80000001; 
   uint256 public constant RANGEEND_11 = 85000000;

而上述分组的变量赋值是用来为后面的不同情况做处理依据。而下面就是关键代码部分:

   function submitTokens(address recipient) payable {
        if (msg.value == 0) {
            throw;
        }
        
        //Permit buying only between 10/09/17 - 10/27/2017 and after 11/01/2017
        if((now > start && now < end) || now > launch)
            {               
                uint256 tokens = msg.value.mul(PRICE).div( 1 ether);
                if(tokens.add(OVERALLSOLD) > MAXTOKENSOLD)
                    {
                    throw;
                    }
        
                //Pre-Sale CAP 10,000,000 check
                if(((tokens.add(OVERALLSOLD)) > RANGEEND_PRESALE) && (now > start && now < end))
                    {
                    throw;
                    }
        
                   
                OVERALLSOLD = OVERALLSOLD.add(tokens);  
    
                // Send UCN to Recipient    
                balances[recipient] = balances[recipient].add(tokens);
     
                // Send Funds to MultiSig
                if (!multisig.send(msg.value)) {
                    throw;
                    }
            }
          else
                {
                throw;
                }
        

首先代码判断用户传入的钱数是否为0 。当时间节点处于10/09/17 - 10/27/2017或者在11/01/2017之后时,可以进入下面的内容。(我认为前一个时间段是测试阶段,而后一个时间为投入使用的实际阶段)

逻辑首先将token根据用户传入的value值进行处理,得到一个转换值。之后判断整个合约的总体量是否超过限制。均通过后便更新用户的钱包余额。最盈利的部分就属!multisig.send(msg.value)。在代码逻辑的最后一步中,合约将钱转给了multisig钱包。

之后,由于用户购买了相应的token,所以合约需要对目前的变量进行更新。

        if(now>start && now <end)
        {
            //Stage Pre-Sale Range 0 - 10,000,000 
            if(OVERALLSOLD >= RANGESTART_PRESALE && OVERALLSOLD <= RANGEEND_PRESALE) 
                {
                PRICE = PRICE_PRESALE - (1 + OVERALLSOLD - RANGESTART_PRESALE).div(FACTOR_PRESALE);
                }
        }

除了提交token的功能外,合约中还出现了submitEther函数。

   function submitEther(address recipient) payable {
     if (msg.value == 0) {
       throw;
     }

     if (!recipient.send(msg.value)) {
       throw;
     }
    
   }

官方网站中提到该合约可以进行提款操作,然而我对代码分析后,发现唯一能够对用户进行转账的地方在此。此处我们需要传入参数recipient地址,然后合约就会向该地址进行转账。转账数额是msg.value???所谓的转账无非就是类似于A向合约转了n个ether,然而合约把这n个ether转给B。然而合约并不会损失任何。

之后,我们再来看世界广播功能。

  struct MessageQueue {
           string message; 
           string from;
           uint expireTimestamp;  
           uint startTimestamp;
           address sender; 
     uint256 public constant maxSpendToken = 3600; //Message should last approx. 1 hour max

     MessageQueue[] public mQueue;
       }

合约定义了结构体变量用于保存各个用户的消息。包括了起止时间以及消息来源等等。

function addMessageToQueue(string msg_from, string name_from, uint spendToken) {
        if(balances[msg.sender]>spendToken && spendToken>=10)
        {
           if(spendToken>maxSpendToken) 
               {
                   spendToken=maxSpendToken;
               }
           
           UniCoinSize=UniCoinSize+spendToken;
           
           balances[msg.sender] = balances[msg.sender].sub(spendToken);
          
          //If first message or last message already expired set newest timestamp
          uint expireTimestamp=now;
          if(mQueue.length>0)
            {
             if(mQueue[mQueue.length-1].expireTimestamp>now)
                {
                expireTimestamp = mQueue[mQueue.length-1].expireTimestamp;  // 如果上一个用户的显示时间还没有到,那么下一个用户从结束处开始
                }
            } 

当用户投食这个独角兽的时候,合约会将用户所发的话记录下来,并根据喂食的金额计算出相应的持续时间。之后根据目前队列中存在的消息时间而更新该用户的消息时间。

        mQueue.push(MessageQueue({
                   message: msg_from, 
                   from: name_from,
                   expireTimestamp: expireTimestamp.add(spendToken)+60,  //give at least approx 60 seconds per msg
                   startTimestamp: expireTimestamp,
                   sender: msg.sender
               }));

之后将相关变量存入队列中。下面是喂食函数:

    function feedUnicorn(uint spendToken) {
    
        if(balances[msg.sender]>spendToken)
            {
            UniCoinSize=UniCoinSize.add(spendToken);
            balances[msg.sender] = balances[msg.sender].sub(spendToken);
            }
        
     } 

当传入spendToken后,判断用户余额是否充足,如果充足的话则改变独角兽的大小,并对余额更新。

四、分析

1 漏洞分析

我们在审计合约的时候应该能够注意到合约编写人使用了安全函数来帮助处理溢出问题,然而当我们仔细研究代码的时候就能发现:

image.png

用于检查溢出的关键函数被注释掉了。也就意味着安全函数跟普通的加减法是没有区别的emmm。

所以我们回过头来在看代码:

image.png

在转账授权之后进行转账,存在了下溢漏洞。我们可以针对此使用两个账户进行恶意操作,以便达到账户溢出的效果。

然而这个合约的漏洞不仅限于此,经过我的审计后发现了更多的溢出点。例如:

image.png

这是转账函数,然而这个转账函数并没有进行余额检测就进行了转账。倘若我的用户没有这么多钱,其还是可以转账成功,并且使自己的余额溢出。而编写者应该是认为默认使用sub安全函数后就可以依靠安全函数的内容进行检查操作,然而他忽略了安全代码的关键部分被注释了这个问题。

2 实验操作

首先我们部署合约:

image.png

切换用户:

image.png

之后模拟用户投入部分eth来换取一定token。传入1 ether得到如下token:

image.png

在该用户下调用approve函数赋予0x4b0897b0513fdc7c541b6d9d7e929c4e5364d2db转账300001的权利。

image.png

查看刚才的操作。

image.png

现在我们切换到0x4b0897b0513fdc7c541b6d9d7e929c4e5364d2db用户下,并进行漏洞转账操作。

image.png

"0x14723a09acff6d2a60dcdf7aa4aff308fddc160c","0x4b0897b0513fdc7c541b6d9d7e929c4e5364d2db", 300001

此时查看0x14723a09acff6d2a60dcdf7aa4aff308fddc160c的余额:

image.png

由于下溢导致了溢出。此时达成了攻击目的。

而除了此处外,所有涉及到sub与add的函数均有可能出现类似的溢出漏洞,所以其余部分利用过程类似,这里就不在演示。

五、相关链接

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

首发于:https://xz.aliyun.com/t/4340

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念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