作者:Al1ex@七芒星實驗室
原文鏈接:https://mp.weixin.qq.com/s/dNZr8OPZVr60bSIONJbHyw

溢出原理

計算機中整數變量有上下界,如果在算術運算中出現越界,即超出整數類型的最大表示范圍,數字便會如表盤上的時針從12到1一般,由一個極大值變為一個極小值或直接歸零,此類越界的情形在傳統的軟件程序中很常見,但是否存在安全隱患取決于程序上下文,部分溢出是良性的(如tcp序號等),甚至是故意引入的(例如用作hash運算等)。

以太坊虛擬機(EVM)為整數指定固定大小的數據類型,這意味著一個整型變量只能有一定范圍的數字表示,例如,一個uint8 ,只能存儲在范圍[0,255]的數字,若試圖存儲256到一個uint8將變成0,不加注意的話,只要沒有檢查用戶輸入又執行計算,導致數字超出存儲它們的數據類型允許的范圍,Solidity中的變量就可以被用來組織攻擊。

溢出實例

高賣低收(CVE-2018-11811)

類型描述:管理員通過修改合約中的參數來制造溢出漏洞,導致用戶提幣轉出token之后,卻收不到ETH(或收到極少量ETH),造成用戶經濟損失。

漏洞實例:合約Internet Node Token (INT)

合約地址:https://cn.etherscan.com/address/0x0b76544f6c413a555f309bf76260d1e02377c02a#code

漏洞位置:L179

img

漏洞危害:用戶提幣之后,無法得到對應數額的ETH;

漏洞原理:sellPrice被修改為精心構造的大數后,可導致amount sellPrice的結果大于整數變量(uint256)最大值,發生整數溢出,從而變為一個極小值甚至歸零。該值在程序語義中是用于計算用戶提幣應得的ETH數量,并在L179進行了校驗,但該值被溢出變為極小值之后可以逃逸L179的校驗,并導致用戶售出token后只能拿到少量的(甚至沒有)ETH。

下溢增持(CVE-2018-11687)

類型描述:管理員在特定條件下,通過調用合約中有漏洞的發幣函數制造下溢,從而實現對自身賬戶余額的任意增加。

漏洞實例:合約Bitcoin Red(BTCR)

合約地址:https://cn.etherscan.com/address/0x6aac8cb9861e42bf8259f5abdc6ae3ae89909e11#code

漏洞位置:L45

img

漏洞危害:管理員執行了一個正常向某個地址進行發幣的操作,實際已經暗中將自身賬戶的余額修改為了一個極大的數;

漏洞原理:distributeBTR()函數的本意是管理員給指定地址發放一定數額的token,并從自身賬戶減少對應的token數量。減少管理員賬戶余額的操作為balances[owner] -= 2000 108 ,運算的結果將被存到balances[owner]中,是一個無符號整數類型。當管理員余額本身少于2000 * 108時,減法計算結果為負值,解釋為無符號整數即一個極大值。

隨意鑄幣 (CVE-2018-11812)

類型描述:管理員調用鑄幣函數給某個地址增加token時,利用溢出漏洞可以突破該函數只能增加token的限制,實際減少該地址的token數量,從而實現對任一賬戶余額的任意篡改(增加或減少)。

漏洞實例:合約PolyAi (AI)

合約地址:https://cn.etherscan.com/address/0x5121e348e897daef1eef23959ab290e5557cf274#code

漏洞位置:L136

img

漏洞危害:管理員可以繞過合約限制,任意篡改所有地址的token余額;

漏洞原理:攻擊者通過構造一個極大的mintedAmount,使得balanceOf[target] + mintedAmount發生整數溢出,計算結果變為一個極小值。

超額鑄幣(CVE-2018-11809)

類型描述:管理員通過構造惡意參數,可以繞過程序中規定的token發行上限,實現超額鑄幣。合約Playkey (PKT)存在此類漏洞,導致合約中的鑄幣上限形同虛設,從而發行任意多的token。此外,我們還發現Nexxus (NXX)、Fujinto (NTO)兩個合約存在類似漏洞,這兩個合約沒有鑄幣上限限制,但同樣的手段,可以溢出合約中一個用于記錄已發幣總量(totalSupply)的變量值,使其與市場中實際流通的總幣數不一致。

漏洞實例:合約Playkey (PKT)

合約地址:https://cn.etherscan.com/address/0x2604fa406be957e542beb89e6754fcde6815e83f#code

漏洞位置:紅色標注的行L241

img

漏洞危害:管理員可以篡改已發幣總量(totalSupply)為任意值,并繞過合約中的鑄幣上限超額發行token;

漏洞原理:_value在函數調用時被設置為精心構造的極大值,使得totalSupply + _value計算結果溢出后小于tokenLimit,從而輕易繞過L237行的鑄幣上限檢測。

超額分配(CVE-2018-11810)

類型描述:管理員通過制造溢出來繞過合約中對單地址發幣的最大上限,可以對指定地址分配超額的token,使得對單地址的發布上限無效。

漏洞實例:合約LGO (LGO)

合約地址:https://cn.etherscan.com/address/0x123ab195dd38b1b40510d467a6a359b201af056f#code

漏洞位置:紅色標注的行L286

img

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

漏洞原理:一個極大的_amount可以使得算數加法運算holdersAllocatedAmount + _amount發生整數溢出,變為一個極小值,從而繞過L286的檢測。

超額購幣(CVE-2018-11809)

漏洞描述:買家如果擁有足夠多的ETH,可以通過發送大量token制造溢出,從而繞過ICO發幣上限,達到超額購幣。

漏洞實例:合約EthLend (LEND)

合約地址:https://cn.etherscan.com/address/0x80fB784B7eD66730e8b1DBd9820aFD29931aab03#code

漏洞位置:紅色標注的行L236

img

漏洞危害:調用者繞過合約中規定ICO的token容量上限,獲得了超額購幣;

漏洞原理:一個極大的_newTokens可以使得算數加法運算totalSoldTokens + newTokens發生整數溢出,變為一個極小值,從而繞過L236的檢測。

溢出調試

減法溢出

案例代碼如下:

pragma solidity ^0.4.22;
contract TokenExample {
    address public owner;
    mapping(address => uint256) public balances;
    mapping(address =>mapping(address =>uint256)) public allowed;

    event Transfer(address _from,address _to,uint256 _value);

    modifier onlyOwner{
        require(msg.sender == owner);
        _;
    }

    constructor() public {
        owner = msg.sender;
        balances[owner] = 2000*10**8;
    }

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

img

如上圖所示,在智能合約中的distribute函數的功能是從owner賬戶向指定的地址列表傳入代幣,但是在對balance[owner]的賬戶做減法運算的時候,未使用SafeMath函數進行數值運算操作,而且也沒有判斷合約的owner是否有足夠的代幣,直接一個循環對owner進行減法處理,這里如果轉出的代幣總量大于owner賬戶余額,那么balance[owner]將會發生下溢,變成一個極大的值,下面在remix中演示操作一下:

編譯合約

img

部署合約

img

下溢操作

調用distribute函數傳入地址數組: ["0x14723a09acff6d2a60dcdf7aa4aff308fddc160c","0x4b0897b0513fdc7c541b6d9d7e929c4e5364d2db"],使用owner分別向這兩個地址發送代幣。 執行之前owner的余額:

img

執行distribute函數:

img

交易日志記錄如下:

img

執行之后owner的余額:

img

可以從上面的結果當中看到合約的owner在執行完distribute函數之后,按理來說轉賬操作應該會使得合約的owner的代幣減少,但是這里去不減反增了,故這里的“下溢”確實存在。

加法溢出

案例: GEMCHAIN

合約地址:https://cn.etherscan.com/address/0xfb340423dfac531b801d7586c98fe31e12a32f31#code

img

如上上圖所示,該智能合約中的mintToken函數用于增發代幣,但是在增發代幣的過程中對于加法操作沒有使用SafeMath函數進行數值運算操作,而且也沒有使用require對是否發生溢出進行檢查,故這里存在溢出風險,如果合約的owner給target增發較多數量的mintedAmount那么將會導致溢出問題的發生。 使用remix演示如下:

編譯合約

img

部署合約

img

上溢操作

第一次鑄幣: 首先,我們先調用mintToken函數向地址“0x14723a09acff6d2a60dcdf7aa4aff308fddc160c”鑄幣,鑄幣的數量為: “0x8000000000000000000000000000000000000000000000000000000000000000”即2的255次方

img

交易日志:

img

鑄幣之后地址“0x14723a09acff6d2a60dcdf7aa4aff308fddc160c”的余額為:

img

為了讓其發生溢出,我們還需要向地址“0x14723a09acff6d2a60dcdf7aa4aff308fddc160c”鑄幣,鑄幣的數量仍然為:“0x8000000000000000000000000000000000000000000000000000000000000000”即2的255次方,目的就是為了讓2的255次方+2的255次方發生溢出,超出uint256的最大范圍。下面具體看操作

第二次鑄幣:

img

交易日志:

img

查看余額:

img

從上面的結果我們可以發現確實發生了溢出!可想而知,如果合約的owner在不校驗溢出問題的情況下向某一地址鑄幣,那么該地址如果發生溢出,那么代幣數量將會發生變化,時而出現減少的情況(因為發生溢出)。

特殊情況

有時候你會發現雖然我們看到一個合約當中有整型溢出的風險,例如在transfer函數中未使用require進行溢出校驗,同時也沒有使用SafeMath函數進行數值運算防護的情形,但是該合約當中已經規定了token的總量(totalSupply),而且沒有鑄幣函數(mintToken)另外增加代幣,那么合約總體來說是安全的,不存在整型溢出,為什么這樣說呢?因為你永遠都不會發生兩個數值相加超過uint256的情況,但是在這中情況下你就應該將目光放到“乘法溢出”或“減法下溢”的問題上來進行查找,審計是否真的不存在“整型溢出”問題。

溢出防御

那么如何防范這種整型溢出問題呢?官方給出的建議是使用OpenZepplin提供的SafeMath函數庫進行數值運算操作,使用SafeMath庫函數可以有效的對溢出問題進行檢查與防范,SafeMath函數庫源代碼如下:

https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/math/SignedSafeMath.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
/**
 * @title SignedSafeMath
 * @dev Signed math operations that revert on error.
 */
library SignedSafeMath {
    /**
     * @dev Returns the multiplication of two signed integers, reverting on
     * overflow.
     *
     * Counterpart to Solidity's `*` operator.
     *
     * Requirements:
     *
     * - Multiplication cannot overflow.
     */
    function mul(int256 a, int256 b) internal pure returns (int256) {
        return a * b;
    }
    /**
     * @dev Returns the integer division of two signed integers. Reverts on
     * division by zero. The result is rounded towards zero.
     *
     * Counterpart to Solidity's `/` operator.
     *
     * Requirements:
     *
     * - The divisor cannot be zero.
     */
    function div(int256 a, int256 b) internal pure returns (int256) {
        return a / b;
    }
    /**
     * @dev Returns the subtraction of two signed integers, reverting on
     * overflow.
     *
     * Counterpart to Solidity's `-` operator.
     *
     * Requirements:
     *
     * - Subtraction cannot overflow.
     */
    function sub(int256 a, int256 b) internal pure returns (int256) {
        return a - b;
    }
    /**
     * @dev Returns the addition of two signed integers, reverting on
     * overflow.
     *
     * Counterpart to Solidity's `+` operator.
     *
     * Requirements:
     *
     * - Addition cannot overflow.
     */
    function add(int256 a, int256 b) internal pure returns (int256) {
        return a + b;
    }
}

應用了SafeMath函數的智能合約實例: https://etherscan.io/address/0xB8c77482e45F1F44dE1745F52C74426C631bDD52#code

img

可以看到在上面的智能合約當中對于數值運算都使用了SafeMath函數進行操作,而且也使用了require對溢出校驗進行防護,總體較為安全。

溢出列表

https://github.com/sec-bit/awesome-buggy-erc20-tokens/blob/59e167f74a8d7cf48eadf25a75c65e461450aea0/raw/totalsupply-overflow.txt

https://github.com/peckshield/vuln_disclosure/blob/7a1e99695945220f4bbc10100f72fa7ecb9e0a79/tradeTrap.csv

https://github.com/BlockChainsSecurity/EtherTokens/blob/6e1e0952bc2a4b213cdc6db6ba7a855d9c776242/GEMCHAIN/mint%20integer%20overflow.md

https://github.com/dwfault/AirTokens/blob/aff7102887096a6c8d384820835818f445f3401f/Link_Platform__LNK_/mint%20integer%20overflow.md

文末總結

整型溢出問題發生的根源還是在于合約的開發者在開發合約時未考慮到“整型溢出”問題,作為審計人員的我們在看到合約時也要保持清醒,對于存在疑惑的地方應該采用“調試、驗證”的方法去排除疑慮,而且在審計的過程中也要十分的認真、細心才可以,不要放過任何一個有可能存在問題的地方,例如修飾器/修飾詞對應的權限問題、邏輯處理問題等等。


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