我们知道在大多数太坊合约均需要一个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声明:
2.Remix-ide等编译器会对constructor的错误使用产生警告,开发者千万不要忽略编译器告警,推荐更改源码,消除所有编译器警告。
首先该漏洞展示的就是这个被人攻击的放置类游戏的智能合约,游戏名为Ether Cartel。与要求玩家孵化并且售卖虾子、之后提高产量再交易虾卵以换取以太币的的Ether Shrimp Farm应用类似,Ether Cartel也是相同的思路,只不过场景换成了某种非法物品交易。你所拥有的量越多,你生产的物品越多(一比一的生产比率)。收集更多的物品就可以加倍生产速度”。
在图中我们能够看出该合约误写了一个普通函数用于改变ceoAddress变量,而这个危害是非常大的。
当我更好账户的时候,我们可以任意调用DrugDealer()
函数,并改变合约的owenr地址。
该漏洞为受ceoAnyone漏洞影响的放置游戏智能合约。Ether Cartel在UTC时间2018年5月18日17点14分56秒部署到主网,不幸的是一小时十一分钟之后即被攻破。存在漏洞的代码在18到20行:可被人以调用方调用的DrugDealer()函数,允许调用者修改收益地址——ceoAddress。
而后拿到了权限后,攻击者能够更进一步的调用其余函数,例如:
即当调用SellDrugs函数时会将fee转入ceoAddress账户,所以当该账户被恶意篡改后,该转账金额就会被窃取。同样下面的函数类似。
本章介绍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为:
切换用户后,我们执行unlockToken()
。
由于我们不是合约的owner,所以该函数无法执行成功。
之后我们调用setowner。
由于修改了owenr的信息,所以onlyOwner
让我们绕过,我们可以恶意增加自己的余额。
除此之外,我们还能将调用
function lockBalances() onlyOwner {
balancesUploaded = true;
}
来进行对函数的限制。
具体函数见:https://etherscan.io/address/0xcdcfc0f66c522fd086a1b725ea3c0eeb9f9e8814#contracts