一、前言
最近在审计代码的时候发现了两则十分相似的代码,审计之后发现代码存在一些问题,后来查询发现两则代码确为2018年的CVE。即:CVE-2018–11411与CVE-2018–10468。由于者的漏洞点十分相似,所以在本文中我将两代码一同进行分析。
其代码分别位于:https://etherscan.io/address/0x27f706edde3aD952EF647Dd67E24e38CD0803DD6#code
https://etherscan.io/address/0xf084d5bc3e35e3d903260267ebd545c49c6013d0#code
经过简单的分析,我发现两则漏洞均是由编写者在编写代码过程中错误撰写合约判断条件从而导致攻击者可以利用条件传入合适的参数从而绕过判断进而作恶。
代码并不长,但是危害性极大。
二、代码分析
由于代码十分相似,所以我们仅分析其不同点与关键点。
合约如下:
/**
* Source Code first verified at https://etherscan.io on Wednesday, June 28, 2017
(UTC) */
pragma solidity ^0.4.10;
contract ForeignToken {
function balanceOf(address _owner) constant returns (uint256);
function transfer(address _to, uint256 _value) returns (bool);
}
contract UselessEthereumToken {
address owner = msg.sender;
bool public purchasingAllowed = false;
mapping (address => uint256) balances;
mapping (address => mapping (address => uint256)) allowed;
uint256 public totalContribution = 0;
uint256 public totalBonusTokensIssued = 0;
uint256 public totalSupply = 0;
function name() constant returns (string) { return "Useless Ethereum Token"; }
function symbol() constant returns (string) { return "UET"; }
function decimals() constant returns (uint8) { return 18; }
function balanceOf(address _owner) constant returns (uint256) { return balances[_owner]; }
function transfer(address _to, uint256 _value) returns (bool success) {
// mitigates the ERC20 short address attack
if(msg.data.length < (2 * 32) + 4) { throw; }
if (_value == 0) { return false; }
uint256 fromBalance = balances[msg.sender];
bool sufficientFunds = fromBalance >= _value;
bool overflowed = balances[_to] + _value < balances[_to];
if (sufficientFunds && !overflowed) {
balances[msg.sender] -= _value;
balances[_to] += _value;
Transfer(msg.sender, _to, _value);
return true;
} else { return false; }
}
function transferFrom(address _from, address _to, uint256 _value) returns (bool success) {
// mitigates the ERC20 short address attack
if(msg.data.length < (3 * 32) + 4) { throw; }
if (_value == 0) { return false; }
uint256 fromBalance = balances[_from];
uint256 allowance = allowed[_from][msg.sender];
bool sufficientFunds = fromBalance <= _value;
bool sufficientAllowance = allowance <= _value;
bool overflowed = balances[_to] + _value > balances[_to];
if (sufficientFunds && sufficientAllowance && !overflowed) {
balances[_to] += _value;
balances[_from] -= _value;
allowed[_from][msg.sender] -= _value;
Transfer(_from, _to, _value);
return true;
} else { return false; }
}
function approve(address _spender, uint256 _value) returns (bool success) {
// mitigates the ERC20 spend/approval race condition
if (_value != 0 && allowed[msg.sender][_spender] != 0) { return false; }
allowed[msg.sender][_spender] = _value;
Approval(msg.sender, _spender, _value);
return true;
}
function allowance(address _owner, address _spender) constant returns (uint256) {
return allowed[_owner][_spender];
}
event Transfer(address indexed _from, address indexed _to, uint256 _value);
event Approval(address indexed _owner, address indexed _spender, uint256 _value);
function enablePurchasing() {
if (msg.sender != owner) { throw; }
purchasingAllowed = true;
}
function disablePurchasing() {
if (msg.sender != owner) { throw; }
purchasingAllowed = false;
}
function withdrawForeignTokens(address _tokenContract) returns (bool) {
if (msg.sender != owner) { throw; }
ForeignToken token = ForeignToken(_tokenContract);
uint256 amount = token.balanceOf(address(this));
return token.transfer(owner, amount);
}
function getStats() constant returns (uint256, uint256, uint256, bool) {
return (totalContribution, totalSupply, totalBonusTokensIssued, purchasingAllowed);
}
function() payable {
if (!purchasingAllowed) { throw; }
if (msg.value == 0) { return; }
owner.transfer(msg.value);
totalContribution += msg.value;
uint256 tokensIssued = (msg.value * 100);
if (msg.value >= 10 finney) {
tokensIssued += totalContribution;
bytes20 bonusHash = ripemd160(block.coinbase, block.number, block.timestamp);
if (bonusHash[0] == 0) {
uint8 bonusMultiplier =
((bonusHash[1] & 0x01 != 0) ? 1 : 0) + ((bonusHash[1] & 0x02 != 0) ? 1 : 0) +
((bonusHash[1] & 0x04 != 0) ? 1 : 0) + ((bonusHash[1] & 0x08 != 0) ? 1 : 0) +
((bonusHash[1] & 0x10 != 0) ? 1 : 0) + ((bonusHash[1] & 0x20 != 0) ? 1 : 0) +
((bonusHash[1] & 0x40 != 0) ? 1 : 0) + ((bonusHash[1] & 0x80 != 0) ? 1 : 0);
uint256 bonusTokensIssued = (msg.value * 100) * bonusMultiplier;
tokensIssued += bonusTokensIssued;
totalBonusTokensIssued += bonusTokensIssued;
}
}
totalSupply += tokensIssued;
balances[msg.sender] += tokensIssued;
Transfer(address(this), msg.sender, tokensIssued);
}
}
改合约很明显为一款ERC20的应用产品,其拥有转账函数、余额查询函数、授权函数、授权转账函数等。
在该合约中使用暂停、开始函数来使合约管理者控制合约的状态。
function enablePurchasing() {
if (msg.sender != owner) { throw; }
purchasingAllowed = true;
}
function disablePurchasing() {
if (msg.sender != owner) { throw; }
purchasingAllowed = false;
}
而在该函数中,为了方便扩展性操作还引入了外币的使用方法:
function withdrawForeignTokens(address _tokenContract) returns (bool) {
if (msg.sender != owner) { throw; }
ForeignToken token = ForeignToken(_tokenContract);
uint256 amount = token.balanceOf(address(this));
return token.transfer(owner, amount);
}
使用此函数能够创建外币合并并传入相应的地址便可进行外币转账操作。
在该合约中如何参与到token的购买呢?具体要看下列函数:
function() payable {
if (!purchasingAllowed) { throw; }
if (msg.value == 0) { return; }
owner.transfer(msg.value);
totalContribution += msg.value;
uint256 tokensIssued = (msg.value * 100);
if (msg.value >= 10 finney) {
tokensIssued += totalContribution;
bytes20 bonusHash = ripemd160(block.coinbase, block.number, block.timestamp);
if (bonusHash[0] == 0) {
uint8 bonusMultiplier =
((bonusHash[1] & 0x01 != 0) ? 1 : 0) + ((bonusHash[1] & 0x02 != 0) ? 1 : 0) +
((bonusHash[1] & 0x04 != 0) ? 1 : 0) + ((bonusHash[1] & 0x08 != 0) ? 1 : 0) +
((bonusHash[1] & 0x10 != 0) ? 1 : 0) + ((bonusHash[1] & 0x20 != 0) ? 1 : 0) +
((bonusHash[1] & 0x40 != 0) ? 1 : 0) + ((bonusHash[1] & 0x80 != 0) ? 1 : 0);
uint256 bonusTokensIssued = (msg.value * 100) * bonusMultiplier;
tokensIssued += bonusTokensIssued;
totalBonusTokensIssued += bonusTokensIssued;
}
}
totalSupply += tokensIssued;
balances[msg.sender] += tokensIssued;
Transfer(address(this), msg.sender, tokensIssued);
}
该函数要求用户传入msg.value且没有金额的限制,且此金额将被合约自动转移到owner的钱包中。如何金额大于10 finney,变会自动进行金额的调整,而此处的调整合约使用了其自身的一套机制,所以我们这里不用深度研究。之后得到tokensIssued
金额,变将用户的余额加上此金额即可。
而对于DimonCoin
合约来说,其最大的不同为出现了distributeFUD
函数。此函数仅由owner调用且用于统一减去传入数组的中所有地址的余额。
而如此简单的合约为什么为存在漏洞呢?这个漏洞点出现在转账函数中。下一章我们详细进行讲解。
三、漏洞利用
在审计此代码时,由于token机制所以应该重点注意转账函数。而此合约中就是由于转账函数出现的问题而导致的漏洞。
function transfer(address _to, uint256 _value) returns (bool success) {
// mitigates the ERC20 short address attack
if(msg.data.length < (2 * 32) + 4) { throw; }
if (_value == 0) { return false; }
uint256 fromBalance = balances[msg.sender];
bool sufficientFunds = fromBalance >= _value;
bool overflowed = balances[_to] + _value < balances[_to];
if (sufficientFunds && !overflowed) {
balances[msg.sender] -= _value;
balances[_to] += _value;
Transfer(msg.sender, _to, _value);
return true;
} else { return false; }
}
上述函数用于直接转账,我们来看判断条件,当转账人余额大于转账金额时,sufficientFunds
为1,当没有出现溢出时overflowed
为0,此时!overflowed
为1。则下面的转账过程变满足条件。而我们来看下面的transferFrom
函数。
function transferFrom(address _from, address _to, uint256 _value) returns (bool success) {
// mitigates the ERC20 short address attack
if(msg.data.length < (3 * 32) + 4) { throw; }
if (_value == 0) { return false; }
uint256 fromBalance = balances[_from];
uint256 allowance = allowed[_from][msg.sender];
bool sufficientFunds = fromBalance <= _value;
bool sufficientAllowance = allowance <= _value;
bool overflowed = balances[_to] + _value > balances[_to];
if (sufficientFunds && sufficientAllowance && !overflowed) {
balances[_to] += _value;
balances[_from] -= _value;
allowed[_from][msg.sender] -= _value;
Transfer(_from, _to, _value);
return true;
} else { return false; }
}
在此函数中我们自己阅读条件就发现了端倪。当正常理解逻辑为,转账人余额要大于转账金额,而条件却为fromBalance <= _value
;除此之外,要满足转账条件balances[_to] + _value > balances[_to]
需要为0。而此时就只有溢出才能满足条件。
正确的代码应该编写如下:
bool sufficientFunds = fromBalance >= _value;
bool sufficientAllowance = allowance >= _value;
bool overflowed = balances[_to] + _value > balances[_to];
if (sufficientFunds && sufficientAllowance && overflowed) {
那么我们来具体看一看如何进行实战利用。
首先我们模拟owner部署该合约。
令owner调用fallback
函数,传入2 ether 作为启动资金。
之后更换用户。令新用户传入10wei。
得到:
为了满足sufficientFunds==1
&&sufficientAllowance==1
&&overflowed==0
。
即使得账户余额小于转账金额、授权金额小于转账金额、且满足溢出hhh。
"0x583031d1113ad414f02576bd6afabfb302140225","0xca35b7d915458ef540ade6068dfe2f44e8fa733c",0xfffffffffffffffffffffffffffffffffffffffffffffde1e61f36454dbfffff
首次调用得到:
第二次调用得到:
即每次调用均会凭空增加许多代币。
攻击账户中只有起始资金1000,且其他合并并没有授权给他收钱的权利,所以传入该参数能够令其凭空窃取。同样,该漏洞在DimonCoin
中同样存在。
调用方法与上述内容相同。
对待这种错误需要合约编写者认真检查所写合约,由于判断不严格而导致的漏洞是致命的,并且很容易被攻击者利用。
四、参考
https://etherscan.io/address/0xf084d5bc3e35e3d903260267ebd545c49c6013d0#code
https://etherscan.io/address/0x27f706edde3aD952EF647Dd67E24e38CD0803DD6#code