一、前言
很久没有对以太坊DAPP进行研究分析了,今天审计代码时偶然发现一款很有趣的DAPP应用。在以往的DAPP研究中,我们总能发现吸引用户投资的地方,例如前几篇文章中我们曾经介绍过Fomo3D
游戏合约,为了吸引用户参与参考了心理学、经济学等理论。它让用户为了最后的大奖而进行疯狂进行投注行为,从而使游戏永远无法停止。而对于庞氏代币合约来说,它吞并了用户的本金并使新用户加入来实现盈利。
而在这次的分析中,我们通过其参与者的官方网站并没有发现其激励机制。也就是说在我看来就是一个很无聊的游戏而已emmm。但是还是吸引了很多玩家进行参与,并将钱均转向某个特定钱包地址。而在代码分析过程中,我还发现了除了DAPP本身之外的代码漏洞。这里一并写在这里。
二、DAPP—SaveUnicoins应用功能介绍
读者可以访问http://www.saveunicoins.com/#来查看该DAPP的官网。
该应用使用了与独角兽(unicorn)相近的英语单词(unicoins)来作为噱头进行应用设计。而查看游戏介绍我们发现其内容很笼统。大致意思如下:
SAVEUNICOINS
是一款建立在以太坊上的DAPP应用,参与者需要“充币”来喂养这个独角兽。然而介绍中说道这里面的Unicoin可以通过官网或者智能合约来换取以太币,不过经过我分析代码后,并没有发现相关的转账代码(这个我稍后进行代码分析)。所以这个DAPP游戏就是令用户不断充钱,然后用什么来激励用户呢?
合约介绍中说明到,我们做这款DAPP是让用户来通过换取的token来喂养我们的独角兽,收到喂养的独角兽就可以不断的长大。而作为奖励,投食的用户可以获得全网广播
的特权,也就是用户可以发一段消息,而这个消息将作为区块链的数据使所有用户均能够看到。对于区块链来说,由于数据是全网区块,所有用户均可以看到。
上图为游戏界面,我们可以通过这两种操作来购买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);
}
在该合约中,我们对balanceOf
与transfer
进行了定义,并且设定了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 漏洞分析
我们在审计合约的时候应该能够注意到合约编写人使用了安全函数来帮助处理溢出问题,然而当我们仔细研究代码的时候就能发现:
用于检查溢出的关键函数被注释掉了。也就意味着安全函数跟普通的加减法是没有区别的emmm。
所以我们回过头来在看代码:
在转账授权之后进行转账,存在了下溢漏洞。我们可以针对此使用两个账户进行恶意操作,以便达到账户溢出的效果。
然而这个合约的漏洞不仅限于此,经过我的审计后发现了更多的溢出点。例如:
这是转账函数,然而这个转账函数并没有进行余额检测就进行了转账。倘若我的用户没有这么多钱,其还是可以转账成功,并且使自己的余额溢出。而编写者应该是认为默认使用sub安全函数后就可以依靠安全函数的内容进行检查操作,然而他忽略了安全代码的关键部分被注释了这个问题。
2 实验操作
首先我们部署合约:
切换用户:
之后模拟用户投入部分eth来换取一定token。传入1 ether得到如下token:
在该用户下调用approve
函数赋予0x4b0897b0513fdc7c541b6d9d7e929c4e5364d2db
转账300001
的权利。
查看刚才的操作。
现在我们切换到0x4b0897b0513fdc7c541b6d9d7e929c4e5364d2db
用户下,并进行漏洞转账操作。
"0x14723a09acff6d2a60dcdf7aa4aff308fddc160c","0x4b0897b0513fdc7c541b6d9d7e929c4e5364d2db", 300001
。
此时查看0x14723a09acff6d2a60dcdf7aa4aff308fddc160c
的余额:
由于下溢导致了溢出。此时达成了攻击目的。
而除了此处外,所有涉及到sub与add的函数均有可能出现类似的溢出漏洞,所以其余部分利用过程类似,这里就不在演示。
五、相关链接
本稿为原创稿件,转载请标明出处。谢谢。