Ethernaut 题库闯关 #10 — Re-entrancy(重入)

Ethernaut题库闯关连载第10篇题解,如何利用重入。

Ethernaut题库闯关连载第10篇, 如何利用重入。

今天这篇是Ethernaut 题库闯关连载的第10篇,难度等级:有点难。

Ethernaut 题库闯关我已经整理为一个专栏了, 欢迎大家订阅专栏。

挑战 10:重入

本关的目标是让你盗走以下合约中的所有资金:

// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

import '@openzeppelin/contracts/math/SafeMath.sol';

contract Reentrance {

  using SafeMath for uint256;
  mapping(address => uint) public balances;

  function donate(address _to) public payable {
    balances[_to] = balances[_to].add(msg.value);
  }

  function balanceOf(address _who) public view returns (uint balance) {
    return balances[_who];
  }

  function withdraw(uint _amount) public {
    if(balances[msg.sender] >= _amount) {
      (bool result,) = msg.sender.call{value:_amount}("");
      if(result) {
        _amount;
      }
      balances[msg.sender] -= _amount;
    }
  }

  receive() external payable {}
}

在查看解题思路之前,可以自己先想一想, 本次挑战涉及到的主要知识点有:

  • Fallback 回退方法

  • Throw/revert 会“冒泡”

另外,我们需要注意:不被信任的合约可能会在你最不期望的地方执行代码。

研究合约

Reentrance合约是一个基本合约,允许用户向一个特定的地址捐赠ETH。该用户可以在以后的时间里来提取他/她所收到的捐款。 让我们回顾一下合约的代码:

状态变量

mapping(address => uint) public balances用于存储用户的余额,以了解他们可以提取的金额。

构造函数

这个合约没有构造函数

donate 捐赠函数

donate函数允许msg.sender将一些ETH捐赠给另一个地址。该函数使用SafeMath进行add操作,但可以肯定的是,它可能永远不会溢出。

function donate(address _to) public payable {
    balances[_to] = balances[_to].add(msg.value);
}

这里在接收方地址没有具体的检查,这可能允许一些奇怪的交互,比如说:

  • 捐赠给合约本身,这将使这些资金永远被锁定。
  • 捐赠给address(0),这将使这些资金永远被锁定。
  • 捐赠给msg.sender本身, 这很奇怪,但以后用户可以通过调用withdraw来取回资金。

balanceOf 函数

这个函数允许查询balances映射变量以了解捐赠给特定地址的ETH数量:

function balanceOf(address _who) public view returns (uint balance) {
    return balances[_who];
}

这里没有什么特别的东西可看。

receive(接收函数)

这是一个允许合约接收任意数量的ETH的函数:

receive() external payable {}

老实说,我看不出有什么理由要有这个函数。这个函数只会给终端用户带来问题,他们被允许向合约发送资金,而这些资金不能在以后提取,因为它们不被 balances变量跟踪。

withdraw 函数

这是我们需要注意的函数,以解决这个难题。让我们看看代码,回顾一下它是如何工作的:

function withdraw(uint256 _amount) public {
    if (balances[msg.sender] >= _amount) {
        (bool result, ) = msg.sender.call{value: _amount}("");
        if (result) {
            _amount;
        }
        balances[msg.sender] -= _amount;
    }
}
  1. 该函数检查msg.sender是否有足够的余额来提取_amount的以太币。

  2. 它通过一个低级别的 call 函数来发送请求的 _amount,该函数将使用所有剩余的 "Gas" 来执行该操作

  3. 老实说,我不知道if语句里面的代码是做什么的 :D 这是一种老式的代码,可能在Solidity 8.0中已经不能使用了。

  4. 它更新了msg.sender的余额,减少了金额。

我可以看到这里有两个大问题!

该合约使用的Solidity版本<8.0,这意味着每一个数学运算都可能遭受到下溢/溢出攻击。该合约也使用了SafeMath来处理uint256,例如在donate函数中就不存在这个问题。但是在 withdraw中,当函数更新发送者的最终余额时,他们没有使用它。不使用它的原因是,合约认为它知道(在正常情况下)不会出现下溢,因为有if (balists[msg.sender] >= _amount)检查。

让我们记住这件事,看看另一个问题。

第二个问题是由于合约没有遵循检查-生效-交互模式(Checks-Effects-Interactions)而引入的。它是什么意思呢?直接引用Solidity文档中的话:

大多数函数会首先进行一些检查(谁调用了这个函数,参数是否在范围内,他们是否发送了...

剩余50%的内容订阅专栏后可查看

点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
Ethernaut CTF
Ethernaut CTF
信奉 CODE IS LAW.