GodGame漏洞原理以及黑客攻击手法分析
在昨日,GodGame智能合约被曝存在漏洞,并且已经被黑客利用了,导致损失了200多个以太币,BCSEC对其进行了详细分析,得出其根本原因是由于某处关键数据整型溢出漏洞导致...
攻击手法
关于攻击手法,之前已经有很多相关区块链安全公司报道过了,不过有些并不是很详尽,BCSEC安全团队对黑客的攻击手法进行了一次比较全面的剖析。
黑客地址:0x2368beb43da49c4323e47399033f5166b5023cda。
黑客的攻击步骤:
这便是该黑客一系列的攻击步骤,乍一看,充满迷雾,攻击合约也没开源,只能靠逆向,经过BCSEC安全团队分析,该攻击合约只是相当于一个“代理”,举个例子,攻击者若调用攻击合约的withdraw函数,那么该攻击合约就会去调用godgame的withdraw函数,然后再把结果返回给攻击者。
那攻击者为何要使用攻击合约间接调用而不直接调用godgame合约,为何要调用这些函数呢?这便是本文的重点了,以下会对攻击者的每个步骤进行剖析。
分析
在理解漏洞之前我们需要先了解一下该合约中几个重要变量,方便后续理解:
1.payoutsTo变量,该变量是用于存储某个用户消费了多少分红
2.profitPerShare,该变量用于存储当前God代币所能产生的分红比例,非固定,是浮动的
3.tokenBalanceLedger,该变量用户存储某个用户的代币余额
4.mydi vidends ,获取当前用户的分红余额
PS:看到上面几个变量,某些同学应该会很熟悉,没错,说得就是PoWH3D,这个合约代码和风靡一时的PoWH3D极其相似,熟悉PoWH3D的应该很容易理解,不熟悉的也没关系,这个看下这篇文章。
那么:这些变量之间的有何关联?
profitPerShare和tokenBalanceLedger的乘积便是某个用户所能得到的分红,但不是mydi vidends(最终的分红余额),因为有payoutsTo(分红支出),所以最终用户的分红余额的简化版计算方式为:
profitPerShare * tokenBalanceLedger - payoutsTo
从上面的公示我们能得到一个结论,那就是profitPerShare * tokenBalanceLedger是必须大于payoutsTo的,否则,在计算最终分红余额的过程中就会发生溢出,将导致显示的分红余额数值非常大。
我们再进一步推导,在没做容错的情况下(该合约确实没对计算最终分红进行容错处理),当profitPerShare * tokenBalanceLedger减少时,payoutsTo的值也需要进行一定量的减少,否则便会出现上述溢出的情况。
再进一步分析,我们检查profitPerShare变量所有可能的更改情况,我们发现该变量不会减少,只会增加,所以我们可以再进一步得出:
当tokenBalanceLedger减少时,payoutsTo也需要进行一定量的减少,否则会产生溢出。
根据合约代码,我们发现只有transfer系列的函数会导致tokenBalanceLedger减少
function transfer(address _toAddress, uint256 _amountOfTokens)
onlyTokenHolders()
public
returns (bool)
{
address _customerAddress = msg.sender;
require(_amountOfTokens >= MIN_TOKEN_TRANSFER
&& _amountOfTokens <= tokenBalanceLedger_[_customerAddress]);
bytes memory empty;
transferFromInternal(_customerAddress, _toAddress, _amountOfTokens, empty);
return true;
}
根据如上代码,transfer函数最终都回调用transferFormInternal函数来进行实际转账,我们继续来看。
function transferFromInternal(address _from, address _toAddress, uint _amountOfTokens, bytes _data)
internal
{
require(_toAddress != address(0x0));
uint fromLength;
uint toLength;
assembly {
fromLength := extcodesize(_from)
toLength := extcodesize(_toAddress)
}
if (fromLength > 0 && toLength <= 0) {
// contract to human
contractAddresses[_from] = true;
contractPayout -= (int) (_amountOfTokens);
tokenSupply_ = SafeMath.add(tokenSupply_, _amountOfTokens);
payoutsTo_[_toAddress] += (int256) (profitPerShare_ * _amountOfTokens);
} else if (fromLength <= 0 && toLength > 0) {
// human to contract
contractAddresses[_toAddress] = true;
contractPayout += (int) (_amountOfTokens);
tokenSupply_ = SafeMath.sub(tokenSupply_, _amountOfTokens);
payoutsTo_[_from] -= (int256) (profitPerShare_ * _amountOfTokens);
} else if (fromLength > 0 && toLength > 0) {
// contract to contract
contractAddresses[_from] = true;
contractAddresses[_toAddress] = true;
} else {
// human to human
payoutsTo_[_from] -= (int256) (profitPerShare_ * _amountOfTokens);
payoutsTo_[_toAddress] += (int256) (profitPerShare_ * _amountOfTokens);
}
// exchange tokens
tokenBalanceLedger_[_from] = SafeMath.sub(tokenBalanceLedger_[_from], _amountOfTokens);
tokenBalanceLedger_[_toAddress] = SafeMath.add(tokenBalanceLedger_[_toAddress], _amountOfTokens);
// to contract
if (toLength > 0) {
ERC223Receiving receiver = ERC223Receiving(_toAddress);
receiver.tokenFallback(_from, _amountOfTokens, _data);
}
// fire event
emit Transfer(_from, _toAddress, _amountOfTokens);
}
太冗长了,我们挑其中的重点看一下:
重点一
} else {
// human to human
payoutsTo_[_from] -= (int256) (profitPerShare_ * _amountOfTokens);
payoutsTo_[_toAddress] += (int256) (profitPerShare_ * _amountOfTokens);
}
// exchange tokens
tokenBalanceLedger_[_from] = SafeMath.sub(tokenBalanceLedger_[_from], _amountOfTokens);
tokenBalanceLedger_[_toAddress] = SafeMath.add(tokenBalanceLedger_[_toAddress], _amountOfTokens);
通过这一段可以看到,在tokenBalanceLedger_[_from]减少时,payoutsTo_[_from]也会进行相应的减少,这是没问题的,但是我们注意到了“human to human”这个注释,难道contract to human还会有不同的情况?紧接着往上看contract to human的部分。
重点二
// contract to human
contractAddresses[_from] = true;
contractPayout -= (int) (_amountOfTokens);
tokenSupply_ = SafeMath.add(tokenSupply_, _amountOfTokens);
payoutsTo_[_toAddress] += (int256) (profitPerShare_ * _amountOfTokens);
注意!在这段代码里面很明显没有对payoutsTo_[_from]有任何减少操作,通过我们上面推导出来的理论,这里没对payoutsTo做处理很可能导致计算最终分红余额的时候产生溢出。
好了,我们再回头看黑客的攻击手法:
前面的不重要,我们直接从黑客的第4个步骤,调用withdraw来解释,先看代码:
function withdraw()
onlyProfitsHolders()
public
{
// setup data
address _customerAddress = msg.sender;
uint256 _di vidends = mydi vidends(false);
// get ref. bonus later in the code
// update di vidend tracker 注意此处,此处会使payoutsTo_增加
payoutsTo_[_customerAddress] += (int256) (_di vidends * magnitude);
// add ref. bonus
_di vidends += referralBalance_[_customerAddress];
referralBalance_[_customerAddress] = 0;
// lambo delivery service
_customerAddress.transfer(_di vidends);
// fire event
emit onWithdraw(_customerAddress, _di vidends);
}
上面的代码中我们自行加了一段注释,调用这个函数的目的很明显,增加payoutsTo的值,使计算最终分红余额的时候有溢出的条件。
然后是第5个步骤,调用transfer函数,通过上面的讲解,这里意义也很明确,减少tokenBalanceLedger的值,因为transfer对contract to human的处理不当,导致tokenBalanceLedger的值减少了,而payoutsTo的值没减少,满足溢出条件。
第6个步骤,调用reinvest函数,因为在第5个步骤中已经使分红余额溢出了,调用reinvest将分红转换成token。
第7个步骤,调用sell函数,因为调用reinvest产生了大量的token,godgame一个游戏机制是,流通的token那么token的价格就越高,产生这批token已经导致token的购买价格为天价,于是攻击者调用sell函数来销毁一部分token让token的价格降到正常,以便自己能顺利提款。
第8个步骤,调用transfer函数,将token转移到另外的账户,然后另外的账户直接将token兑换成分红后提现。
最新评论