合约溢出漏洞
1.溢出事件
1.1 BEC合约
2018年4月22日,黑客对BEC智能合约发起攻击,凭空取出:57,896,044,618,658,100,000,000,000,000,000,000,000,000,000,000,000,000,000,000.792003956564819968个BEC代币并在市场上进行抛售,BEC随即急剧贬值,价值几乎为0,该市场瞬间土崩瓦解。
1.2 SMT项目
2018年4月25日,SMT项目方发现其交易存在异常,黑客利用其函数漏洞创造了: 65,133,050,195,990,400,000,000,000,000,000,000,000,000,000,000,000,000,000,000+50,659,039,041,325,800,000,000,000,000,000,000,000,000,000,000,000,000,000,000的SMT币,火币Pro随即暂停了所有币种的充值提取业务。
1.3 FNT合约
2018年12月27日,以太坊智能合约Fountain(FNT)出现整数溢出漏洞,黑客利用其函数漏洞创造了: 2+115792089237316195423570985008687907853269984665640564039457584007913129639935的SMT币。
2.整数溢出简介
2.1 整数溢出原理
由于计算机底层是二进制,任何十进制数字都会被编码到二进制。溢出会丢弃最高位,导致数值不正确。如:八位无符号整数类型的最大值是255,翻译到二进制是1111 1111;当再加一时,当前所有的1都会变成0,并向上进位。但由于该整数类型所能容纳的位置已经全部是1了,再向上进位,最高位会被丢弃,于是二进制就变成了0000 0000。(注:有符号的整数类型,其二进制最高位代表正负。所以该类型的正数溢出会变成负数,而不是零。)
2.2 solidity中的整数溢出
在solidity中,对于0.8.0以下的版本,也存在整数溢出问题。解决办法就是使用SafeMath库来修补溢出漏洞,源码如下:
library SafeMath {
function mul(uint256 a, uint256 b) internal constant returns (uint256) {
uint256 c = a * b;
assert(a == 0 || c / a == b);
return c;
}
function div(uint256 a, uint256 b) internal constant returns (uint256) {
uint256 c = a / b;
return c;
}
function sub(uint256 a, uint256 b) internal constant returns (uint256) {
assert(b <= a);
return a - b;
}
function add(uint256 a, uint256 b) internal constant returns (uint256) {
uint256 c = a + b;
assert(c >= a);
return c;
}
}
solidity 0.8以上的版本发生运行时溢出会直接revert,修复的方式就是不允许溢出。
3.攻击模拟
3.1 源码如下:
contract ZT is ERC20("ZERO TOKEN", "ZT"){
//RULE 1
bytes32 constant RULE_WITHDRAW_WANT = keccak256(abi.encodePacked("withdraw"));
//RULE 2
bytes32 constant RULE_NONE_WANT = keccak256(abi.encodePacked("depositByValue"));
constructor()public{
_mint(msg.sender,10000000*10**18);
}
function depositByWant(uint _amount)external payable{
uint amount = _amount.mul(10**18);
require(msg.value>=amount,"you want to trick me?");
MkaheChange(msg.sender,amount,RULE_NONE_WANT);
}
function withdraw(uint _amount)external payable returns(bool){
uint amount = _amount.mul(10**18);
require(balanceOf(msg.sender)>=amount);
_balances[msg.sender] = _balances[msg.sender].sub(amount);
return MkaheChange(msg.sender,amount,RULE_WITHDRAW_WANT);
}
function MkaheChange(address to,uint amount,bytes32 ID)internal returns(bool){
if(ID==RULE_NONE_WANT)
{
_balances[msg.sender]=_balances[msg.sender].add(amount);
return true;
}else if(ID==RULE_WITHDRAW_WANT){
bool a;
(a,)=payable(to).call.value(amount)("");
require(a,"withdraw fail");
return true;
}
else{
return false;
}
}
fallback()external payable{
MkaheChange(
msg.sender,
msg.value,
RULE_NONE_WANT
);
}
}
contract ZTstakepool{
ZT token;
uint totalsupply;
string symbol;
mapping(address=>uint)internal workbalance;
mapping(address=>bool)internal passed;
struct userInfo{
uint amount;
uint duration;
uint startTime;
}
mapping(address=>userInfo)internal userDetails;
constructor()public{
token =new ZT();
symbol = "stTGT";
totalsupply = token.balanceOf(address(this));
}
function getDetails(address account)public view returns(userInfo memory){
return userDetails[account];
}
function workBalanceOf(address account)public view returns(uint){
bool pass=passed[account];
if(pass){
return workbalance[account];
}else{
return 0;
}
}
function Zt()public view returns(address){
return address(token);
}
function stake(uint amount,uint blocknumber)external{
require(blocknumber>=1,"At least 1 block");
userInfo storage user = userDetails[msg.sender];
user.startTime = block.number;
user.duration = blocknumber;
user.amount += amount;
token.transferFrom(msg.sender,address(this),amount*10**18);
workbalance[msg.sender] += blocknumber;
}
function unstake()external{
userInfo storage user = userDetails[msg.sender];
require(block.number>=user.startTime+user.duration,"you are in a hurry ");
passed[msg.sender] = true;
uint amount = user.amount;
user.amount = 0;
token.transfer(msg.sender,amount*10**18);
}
function swap(address from,address to,uint amount)external{
require(from==address(this)&&to==address(token));
uint balance = workBalanceOf(msg.sender);
require(balance>=amount,"exceed");
workbalance[msg.sender] -= amount;
token.transfer(msg.sender,amount*10**18);
}
}
contract setup{
ZTstakepool public stakePool;
ZT public erc20;
bool solve;
constructor()public{
stakePool =new ZTstakepool();
erc20 = ZT(payable(stakePool.Zt()));
}
function isSolved()public view returns(bool){
return solve;
}
function complete()public{
require(erc20.balanceOf(msg.sender)>=500000*10**18);
solve = true;
}
}
3.2 合约漏洞查找
在源码中我们可以看到,其它两个合约由合约setup生成,所以我们先部署setup合约,通过观察可以发现要想isSolved函数返回true,需要满足erc20.balanceOf(msg.sender)>=500000*10**18,即产生溢出,因此攻击此合约只需要调用参数满足溢出条件即可,我们在上面的代码中可以看到,溢出可能发生在swap函数中。