作者:Hcamael@知道創宇404區塊鏈安全研究團隊

背景

最近學習了下以太坊的智能合約,而且也看到挺多廠家pr智能合約相關的漏洞,其中《ERC20智能合約整數溢出系列漏洞披露》文章中披露了6個CVE編號的漏洞,而這些漏洞都屬于整型溢出漏洞范疇,其中5個漏洞均需要合約Owner才能觸發利用。本文正是針對這些漏洞從合約代碼及觸發邏輯上做了詳細分析,并提出了一些關于owner相關漏洞的思考。

漏洞分析

1. CVE-2018-11809

該漏洞被稱為“超額購幣”,相關合約(EthLendToken)源碼: https://etherscan.io/address/0x80fB784B7eD66730e8b1DBd9820aFD29931aab03#code

在合約代碼中,buyTokensPresalebuyTokensICO兩個函數都是存在整型上溢出的情況:

function buyTokensPresale() public payable onlyInState(State.PresaleRunning)
    {
        // min - 1 ETH
        require(msg.value >= (1 ether / 1 wei));
        uint newTokens = msg.value * PRESALE_PRICE;

        require(presaleSoldTokens + newTokens <= PRESALE_TOKEN_SUPPLY_LIMIT);

        balances[msg.sender] += newTokens;
        supply+= newTokens;
        presaleSoldTokens+= newTokens;
        totalSoldTokens+= newTokens;

        LogBuy(msg.sender, newTokens);
    }

    function buyTokensICO() public payable onlyInState(State.ICORunning)
    {
        // min - 0.01 ETH
        require(msg.value >= ((1 ether / 1 wei) / 100));
        uint newTokens = msg.value * getPrice();

        require(totalSoldTokens + newTokens <= TOTAL_SOLD_TOKEN_SUPPLY_LIMIT);

        balances[msg.sender] += newTokens;
        supply+= newTokens;
        icoSoldTokens+= newTokens;
        totalSoldTokens+= newTokens;

        LogBuy(msg.sender, newTokens);
    }

溢出點:

require(presaleSoldTokens + newTokens <= PRESALE_TOKEN_SUPPLY_LIMIT);
require(totalSoldTokens + newTokens <= TOTAL_SOLD_TOKEN_SUPPLY_LIMIT);

buyTokensPresale函數舉例,在理論上presaleSoldTokens + newTokens存在整型上溢出漏洞,會導致繞過require判斷,造成超額購幣。

接下來,我們再仔細分析一下,如果造成整型上溢出,先來看看presaleSoldTokens變量的最大值

uint public presaleSoldTokens = 0;
require(presaleSoldTokens + newTokens <= PRESALE_TOKEN_SUPPLY_LIMIT);
presaleSoldTokens+= newTokens;

該合約代碼中,presaleSoldTokens變量相關的代碼只有這三行,因為存在著require判斷,所以不論presaleSoldTokens + newTokens是否溢出,presaleSoldTokens <= PRESALE_TOKEN_SUPPLY_LIMIT恒成立,因為有著斷言代碼:

assert(PRESALE_TOKEN_SUPPLY_LIMIT==60000000 * (1 ether / 1 wei));

所以,presaleSoldTokens <= 60000000 * (1 ether / 1 wei),其中1 ether / 1 wei = 1000000000000000000,所以max(presaleSoldTokens) == 6*(10^25)

再來看看變量newTokens,該變量的值取決于用戶輸出,是用戶可控變量,相關代碼如下:

uint newTokens = msg.value * PRESALE_PRICE;
uint public constant PRESALE_PRICE = 30000;

如果我們向buyTokensPresale函數轉賬1 ether, newTokens的值為1000000000000000000*30000=3*(10^22)

下面來計算一下,需要向該函數轉賬多少以太幣,才能造成溢出

在以太坊智能合約中,uint默認代表的是uint256,取值范圍是0~2^256-1,所以,需要newTokens的值大于(2^256-1)-presaleSoldTokens

最后計算出,我們需要向buyTokensPresale函數轉賬:

>>> (2**256-1)-(6*(10**25))/(3*(10**22))
115792089237316195423570985008687907853269984665640564039457584007913129637935L

才可以造成整型上溢出,超額購幣,整個以太坊公鏈,發展至今,以太幣總余額有達到這個數嗎?

雖然理論上該合約的確存在漏洞,但是實際卻無法利用該漏洞

2. CVE-2018-11810

該類漏洞被稱為:“超額定向分配”

相關事例( LGO )源碼:https://etherscan.io/address/0x123ab195dd38b1b40510d467a6a359b201af056f#code

根據該漏洞的描述:

管理員繞過合約中規定的單地址發幣上限,給指定地址分配超額的token

跟上一個漏洞相比,因為該漏洞存在于onlyOwner的函數中,只能Owner(管理員)才能調用該漏洞,所以我認為該類漏洞可以算做是“后門“類漏洞。

所以該類漏洞的利用有兩個思路:

  1. Owner留下來的“后門”,供自己使用,專門用來坑合約的其他使用者(所謂的”蜜罐合約“,就是這種情況)
  2. 該合約有其他漏洞,能讓自己成為Owener,或者可以說,結合提權漏洞進行利用

首先,我們先假設自己就是Owner,來研究該漏洞的利用流程,以下是存在漏洞的函數:

function allocate(address _address, uint256 _amount, uint8 _type) public onlyOwner returns (bool success) {
        // one allocations by address
        require(allocations[_address] == 0);

        if (_type == 0) { // advisor
            // check allocated amount
            require(advisorsAllocatedAmount + _amount <= ADVISORS_AMOUNT);
            // increase allocated amount
            advisorsAllocatedAmount += _amount;
            // mark address as advisor
            advisors[_address] = true;
        } else if (_type == 1) { // founder
            // check allocated amount
            require(foundersAllocatedAmount + _amount <= FOUNDERS_AMOUNT);
            // increase allocated amount
            foundersAllocatedAmount += _amount;
            // mark address as founder
            founders[_address] = true;
        } else {
            // check allocated amount
            require(holdersAllocatedAmount + _amount <= HOLDERS_AMOUNT + RESERVE_AMOUNT);
            // increase allocated amount
            holdersAllocatedAmount += _amount;
        }
        // set allocation
        allocations[_address] = _amount;
        initialAllocations[_address] = _amount;

        // increase balance
        balances[_address] += _amount;

        // update variables for bonus distribution
        for (uint8 i = 0; i < 4; i++) {
            // increase unspent amount
            unspentAmounts[BONUS_DATES[i]] += _amount;
            // initialize bonus eligibility
            eligibleForBonus[BONUS_DATES[i]][_address] = true;
            bonusNotDistributed[BONUS_DATES[i]][_address] = true;
        }

        // add to initial holders list
        initialHolders.push(_address);

        Allocate(_address, _amount);

        return true;
    }

該合約相當于一個代幣分配的協議,Owner可以隨意給人分配代幣,但是不能超過如下的限制:

代幣的總額: uint256 constant INITIAL_AMOUNT = 100 * onePercent; 給顧問5%: uint256 constant ADVISORS_AMOUNT = 5 * onePercent; 創始人要15%: uint256 constant FOUNDERS_AMOUNT = 15 * onePercent; 銷售出了60%: uint256 constant HOLDERS_AMOUNT = 60 * onePercent; 保留了20%: uint256 constant RESERVE_AMOUNT = 20 * onePercent;

對應到下面三個判斷:

require(advisorsAllocatedAmount + _amount <= ADVISORS_AMOUNT);
require(foundersAllocatedAmount + _amount <= FOUNDERS_AMOUNT);
require(holdersAllocatedAmount + _amount <= HOLDERS_AMOUNT + RESERVE_AMOUNT);

跟上一個CVE一樣,該漏洞本質上也是整型上溢出,但是上一個漏洞,用戶可控的變量來至于向合約轉賬的以太幣的數值,所以在實際情況中,基本不可能利用。但是在該漏洞中,用戶可控的變量_amount,是由用戶任意輸入,使得該漏洞得以實現

下面,利用漏洞給顧問分配超過5%的代幣:

  1. 給顧問A分配2*onePercent數量的代幣:allocte("0xbd08e0cddec097db7901ea819a3d1fd9de8951a2", 362830104000000, 0)

  1. 給顧問B分配一個巨大數量的代幣,導致溢出:allocte("0x63ac545c991243fa18aec41d4f6f598e555015dc", 115792089237316195423570985008687907853269984665640564039457583645083025639937, 0)

  2. 查看顧問B的代幣數:balanceOf("0x63ac545c991243fa18aec41d4f6f598e555015dc") => 115792089237316195423570985008687907853269984665640564039457583645083025639937

經過后續的審計,發現該合約代碼中的own變量只能由Owner修改,所以該漏洞只能被Owner利用

3. CVE-2018-11809

該漏洞被稱為:”超額鑄幣“,但實際和之前的漏洞沒啥區別

含有該漏洞的合約Playkey (PKT)源碼:https://etherscan.io/address/0x2604fa406be957e542beb89e6754fcde6815e83f#code

存在漏洞的函數:

  function mint(address _holder, uint256 _value) external icoOnly {
    require(_holder != address(0));
    require(_value != 0);
    require(totalSupply + _value <= tokenLimit);

    balances[_holder] += _value;
    totalSupply += _value;
    Transfer(0x0, _holder, _value);
  }

比上一個漏洞的代碼還更簡單,只有ico(相當于之前的owner)能執行該函數,閱讀全篇代碼,ico是在合約部署的時候由創建人設置的,后續無法更改,所以該漏洞只能被ico(owner)利用

該合約本身的意圖是,ico能隨意給人分配代幣,但是發行代幣的總額度不能超過tokenLimit,但是通過整型上溢出漏洞,能讓ico發行無限個代幣,利用流程如下:

  1. 部署合約,設置ico為自己賬戶地址,設置發行代幣的上限為100000: PTK("0x8a0b358029b81a52487acfc776fecca3ce2fbf4b", 100000)

  1. 給賬戶A分配一定額度的代幣: mint("0xbd08e0cddec097db7901ea819a3d1fd9de8951a2", 50000)

  1. 利用整型上溢出給賬戶B分配大量的代幣: mint("0x63ac545c991243fa18aec41d4f6f598e555015dc", 115792089237316195423570985008687907853269984665640564039457584007913129589938)
  2. 查看賬戶B的余額: balanceOf("0x63ac545c991243fa18aec41d4f6f598e555015dc") => 115792089237316195423570985008687907853269984665640564039457584007913129589938

4. CVE-2018-11812

該漏洞被稱為:“隨意鑄幣”

相關漏洞合約 Polymath (POLY)源碼:https://etherscan.io/address/0x9992ec3cf6a55b00978cddf2b27bc6882d88d1ec#code

具有漏洞的函數:

function mintToken(address target, uint256 mintedAmount) onlyOwner {
    balanceOf[target] += mintedAmount;
    Transfer(0, owner, mintedAmount);
    Transfer(owner, target, mintedAmount);
}

這個漏洞很簡單,也很好理解,Owner可以隨意增加任意賬戶的代幣余額,可以想象成,銀行不僅能隨心所欲的印鈔票,還能隨心所以的扣你的錢

因為Owner是在合約部署的時候被設置成合約部署者的賬戶地址,之后也只有Owner能修改Own賬戶地址,所以該漏洞只能被Owner利用

這個我覺得與其說是漏洞,不如說是Owner留下的“后門”

5. CVE-2018-11687

該漏洞被稱為:“下溢增持”

相關漏洞合約Bitcoin Red (BTCR)源碼:https://etherscan.io/address/0x6aac8cb9861e42bf8259f5abdc6ae3ae89909e11#code

相關的漏洞函數:

function distributeBTR(address[] addresses) onlyOwner {
    for (uint i = 0; i < addresses.length; i++) {
        balances[owner] -= 2000 * 10**8;
        balances[addresses[i]] += 2000 * 10**8;
        Transfer(owner, addresses[i], 2000 * 10**8);
    }
}

該合約限制了發行代幣的上限: uint256 _totalSupply = 21000000 * 10**8;

并且在合約部署的時候把能發行的合約都分配給了Owner: balances[owner] = 21000000 * 10**8;

然后Owner可以把自己賬戶的代幣,任意分配給其他賬戶,分配的代碼就是上面的函數,給別人分配一定額度的代幣時,自己減去相應額度的代幣,保證該合約總代幣數不變

但是因為沒有判斷Owner的賬戶是否有足夠的余額,所以導致了減法的整型下溢出,同樣也存在整型上溢出,但是因為uint256的上限是2^256-1,但是利用過于繁瑣,需要運行非常多次的balances[addresses[i]] += 2000 * 10**8;

而減法的利用就很簡單了,或者我們可以根本不考慮這個減法,Owner可以給任意賬戶分配2000 * 10**8倍數的代幣,該漏洞的功能和上一個漏洞的基本一致,可以任意發行代幣或者減少其他賬戶的代幣數

因為Owner是在合約部署的時候被設置為部署合約人的賬戶地址,后續沒有修改own的功能,所以該漏洞也只有Owner可以利用

6. CVE-2018-11811

該漏洞被稱為:“高賣低收”

相關漏洞合約 Internet Node Token (INT)源碼:https://etherscan.io/address/0x0b76544f6c413a555f309bf76260d1e02377c02a

在該CVE的描述中,存在漏洞的函數是:

function sell(uint256 amount) {
  require(this.balance >= amount * sellPrice);      // checks if the contract has enough ether to buy
  _transfer(msg.sender, this, amount);              // makes the transfers
  msg.sender.transfer(amount * sellPrice);          // sends ether to the seller. It's important to do this last to avoid recursion attacks
}

并且描述的漏洞原理是:

sellPrice被修改為精心構造的大數后,可導致amount * sellPrice的結果大于整數變量(uint256)最大值,發生整數溢出,從而變為一個極小值甚至歸零`

相關函數如下:

function buy() payable {
  uint amount = msg.value / buyPrice;               // calculates the amount
  _transfer(this, msg.sender, amount);              // makes the transfers
}

function setPrices(uint256 newSellPrice, uint256 newBuyPrice) onlyOwner {
  sellPrice = newSellPrice;
  buyPrice = newBuyPrice;
}

該漏洞的利用流程如下:

  1. 管理員設置buyPrice = 1 ether, sellPrice = 2^255
  2. 用戶A買了兩個以太幣價格的代幣: buy({value:toWei(2)})
  3. 用戶A賣掉兩個代幣: send(2)
  4. 用戶A將會收到2*sellPrice = 2^256價格的Wei
  5. 但是因為transfer的參數是uint256, 所以發生了溢出,用戶A實際得到0Wei

表面上看這個漏洞還是有危害的,但是我們仔細想想,這個漏洞其實是比較多余的,我們可以使用更簡單的步驟達到相同的目的:

  1. 管理員設置buyPrice = 1 ether, sellPrice = 0
  2. 用戶A買了兩個以太幣價格的代幣: buy({value:toWei(2)})
  3. 用戶A賣掉兩個代幣: send(2)
  4. 用戶A將會收到2*sellPrice = 0價格的Wei

我認為該合約最大的問題在于Owner可以隨意設置代幣的買入和賣出價格。

順帶提一下這個問題也是前面peckshield公布的“tradeTrap”漏洞(https://peckshield.com/2018/06/11/tradeTrap/)提到的“Security Issue 2: Manipulatable Prices and Unfair Arbitrage” 是同一個問題。

總結

經過上面的分析,在這6個CVE中,雖然都是整型溢出,但第一個CVE屬于理論存在,但實際不可實現的整型上溢出漏洞,剩下5個CVE都屬于對管理者有利,會損害用戶利用的漏洞,或者可以稱為“后門”,也正是這個原因也導致了一些關于需要Owner觸發漏洞意義討論

如果我們把智能合約類比為傳統合同,智能合約代碼就是傳統合同的內容,但是和傳統的合同相比,智能合約擁有三個利益團體,一個是編寫合約代碼的人(智能合約中的Owner,或者我們可以稱為甲方),使用該合約的其他人(我們可以稱為乙方),跟該智能合約無關的其他人(比如利用合約漏洞獲利的黑客)。從這個角度來看Owner條件下觸發的漏洞在理論上是可以損害到乙方的利益,如對于存在“惡意”的owner或者黑客配合其他漏洞獲取到owner權限的場景上來說,還是有一定意義的。

另外從整個上市交易流程來看,我們還需要關注到“交易所”這個環節,交易所的風控體系在某種程度上可以限制這種“惡意”的owner或黑客利用。

由此可見合約審計對于“甲方”、“乙方”、交易所都有重要的意義。

知道創宇智能合約安全審計:http://www.scanv.com/lca/index.html

歡迎掃碼咨詢:

參考鏈接

  1. http://www.bjnorthway.com/626/
  2. https://mp.weixin.qq.com/s/6PKWXJXAKVCu5bJYlKy2Aw

Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/627/