作者:昏鴉@知道創宇404區塊鏈安全研究團隊
時間:2020年9月14日

發生了什么

iToken是bZx推出的一種代幣,今天早些時候,bZx官方發推表示發現了一些iTokens的安全事件,隨后有研究員對比iToken合約源碼改動,指出其中存在安全問題,可被攻擊用于薅羊毛。

什么是iToken

iToken是bZx推出的類似iDAI、iUSDC的累積利息的代幣,當持有時,其價值會不斷上升。iToken代表了借貸池中的份額,該池會隨借貸人支付利息而擴大。iToken同樣能用于交易、用作抵押、或由開發人員組成結構化產品,又或者用于安全價值存儲。

分析

根據推文指出的代碼,問題存在于_internalTransferFrom函數中,未校驗fromto地址是否不同。

若傳入的fromto地址相同,在前后兩次更改余額時balances[_to] = _balancesToNew將覆蓋balances[_from] = _balancesFromNew的結果,導致傳入地址余額無代價增加。

uint256 _balancesFrom = balances[_from];
uint256 _balancesTo = balances[_to];

require(_to != address(0), "15");

uint256 _balancesFromNew = _balancesFrom.sub(_value, "16");
balances[_from] = _balancesFromNew;

uint256 _balancesToNew = _balancesTo.add(_value);
balances[_to] = _balancesToNew;//knownsec// 變量覆蓋,當_from與_to相同時

漏洞復現

截取transferFrom_internalTransferFrom函數作演示,測試合約代碼如下:

pragma solidity ^0.5.0;

library SafeMath {

    function add(uint256 a, uint256 b) internal pure returns (uint256) {
        uint256 c = a + b;
        require(c >= a, "SafeMath: addition overflow");

        return c;
    }

    function sub(uint256 a, uint256 b) internal pure returns (uint256) {
        return sub(a, b, "SafeMath: subtraction overflow");
    }

    function sub(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) {
        require(b <= a, errorMessage);
        uint256 c = a - b;

        return c;
    }

    function mul(uint256 a, uint256 b) internal pure returns (uint256) {
        if (a == 0) {
            return 0;
        }

        uint256 c = a * b;
        require(c / a == b, "SafeMath: multiplication overflow");

        return c;
    }

    function div(uint256 a, uint256 b) internal pure returns (uint256) {
        return div(a, b, "SafeMath: division by zero");
    }

    function div(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) {
        require(b > 0, errorMessage);
        uint256 c = a / b;
        // assert(a == b * c + a % b); // There is no case in which this doesn't hold

        return c;
    }

    function mod(uint256 a, uint256 b) internal pure returns (uint256) {
        return mod(a, b, "SafeMath: modulo by zero");
    }

    function mod(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) {
        require(b != 0, errorMessage);
        return a % b;
    }
}

contract Test {
    using SafeMath for uint256;
    uint256 internal _totalSupply;
    mapping(address => mapping (address => uint256)) public allowed;
    mapping(address => uint256) internal balances;

    event Transfer(address indexed from, address indexed to, uint256 value);
    event Approval(address indexed owner, address indexed spender, uint256 amount);

    constructor() public {
        _totalSupply = 1 * 10 ** 18;
        balances[msg.sender] = _totalSupply;
    }

    function totalSupply() external view returns (uint256) {
        return _totalSupply;
    }

    function balanceOf(address account) external view returns (uint256) {
        return balances[account];
    }

    function approve(address spender, uint256 amount) external returns (bool) {
        require(spender != address(0));
        allowed[msg.sender][spender] = amount;
        emit Approval(msg.sender, spender, amount);
    }

    function transferFrom(
        address _from,
        address _to,
        uint256 _value)
        external
        returns (bool)
    {
        return _internalTransferFrom(
            _from,
            _to,
            _value,
            allowed[_from][msg.sender]
            /*ProtocolLike(bZxContract).isLoanPool(msg.sender) ?
                uint256(-1) :
                allowed[_from][msg.sender]*/
        );
    }

    function _internalTransferFrom(
        address _from,
        address _to,
        uint256 _value,
        uint256 _allowanceAmount)
        internal
        returns (bool)
    {
        if (_allowanceAmount != uint256(-1)) {
            allowed[_from][msg.sender] = _allowanceAmount.sub(_value, "14");
        }

        uint256 _balancesFrom = balances[_from];
        uint256 _balancesTo = balances[_to];

        require(_to != address(0), "15");

        uint256 _balancesFromNew = _balancesFrom
            .sub(_value, "16");
        balances[_from] = _balancesFromNew;

        uint256 _balancesToNew = _balancesTo
            .add(_value);
        balances[_to] = _balancesToNew;//knownsec// 變量覆蓋,當_from與_to一致時

        // handle checkpoint update
        // uint256 _currentPrice = tokenPrice();

        // _updateCheckpoints(
        //     _from,
        //     _balancesFrom,
        //     _balancesFromNew,
        //     _currentPrice
        // );
        // _updateCheckpoints(
        //     _to,
        //     _balancesTo,
        //     _balancesToNew,
        //     _currentPrice
        // );

        emit Transfer(_from, _to, _value);
        return true;
    }
}

remix部署調試,0x1e9c2524Fd3976d8264D89E6918755939d738Ed5部署合約,擁有代幣總量,授權0x28deb6CA32C274f7DabF2572116863f39b4E65D9500代幣額度

通過0x28deb6CA32C274f7DabF2572116863f39b4E65D9賬戶,調用transferFrom函數,_from_to傳入地址0x1e9c2524Fd3976d8264D89E6918755939d738Ed5_value傳入授權的500

最后查看0x1e9c2524Fd3976d8264D89E6918755939d738Ed5地址余額,已增加500額度,超出代幣發行總量。

綜上,惡意用戶可創建小號,通過不斷授權給小號一定額度,使用小號頻繁為大號刷代幣,增發大量代幣薅羊毛。

總結

針對本次事件,根本原因,還是沒做好上線前的代碼審計工作。由于區塊鏈智能合約的特殊性,智能合約上線前務必做好完善的代碼審計、風險分析的工作。

另外通過github搜索到其他項目也同樣存在這個問題,務必提高警惕。


智能合約審計服務

針對目前主流的以太坊應用,知道創宇提供專業權威的智能合約審計服務,規避因合約安全問題導致的財產損失,為各類以太坊應用安全保駕護航。

知道創宇404智能合約安全審計團隊: https://www.scanv.com/lca/index.html
聯系電話:(086) 136 8133 5016(沈經理,工作日:10:00-18:00)

歡迎掃碼咨詢:

區塊鏈行業安全解決方案

黑客通過DDoS攻擊、CC攻擊、系統漏洞、代碼漏洞、業務流程漏洞、API-Key漏洞等進行攻擊和入侵,給區塊鏈項目的管理運營團隊及用戶造成巨大的經濟損失。知道創宇十余年安全經驗,憑借多重防護+云端大數據技術,為區塊鏈應用提供專屬安全解決方案。

歡迎掃碼咨詢:

參考

https://bzx.network/blog/incident

https://twitter.com/k06a/status/1305223411615117322


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