GodGame漏洞原理以及黑客攻击手法分析

BCSEC  112天前

1535020316309771.png在昨日,GodGame智能合约被曝存在漏洞,并且已经被黑客利用了,导致损失了200多个以太币,BCSEC对其进行了详细分析,得出其根本原因是由于某处关键数据整型溢出漏洞导致...

攻击手法

关于攻击手法,之前已经有很多相关区块链安全公司报道过了,不过有些并不是很详尽,BCSEC安全团队对黑客的攻击手法进行了一次比较全面的剖析。

黑客地址:0x2368beb43da49c4323e47399033f5166b5023cda。

黑客的攻击步骤:

  1. 攻击者购买少量token;

  2. 创建攻击合约;

  3. 把token转给攻击合约;

  4. 调用攻击合约的withdraw函数

  5. 调用攻击合约的transfer函数

  6. 调用攻击合约的reinvest函数

  7. 调用攻击合约的sell函数

  8. 调用攻击合约的transfer函数

这便是该黑客一系列的攻击步骤,乍一看,充满迷雾,攻击合约也没开源,只能靠逆向,经过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兑换成分红后提现。

最新评论

昵称
邮箱
提交评论

友情链接:FOFA FOEYE BCSEC BAIMAOHUI 安全客 i春秋

nosec.org All Rights Reserved 京ICP备15042518号