Ethernaut题库闯关连载第10篇题解,如何利用重入。
Ethernaut题库闯关连载第10篇, 如何利用重入。
今天这篇是Ethernaut 题库闯关连载的第10篇,难度等级:有点难。
Ethernaut 题库闯关我已经整理为一个专栏了, 欢迎大家订阅专栏。
本关的目标是让你盗走以下合约中的所有资金:
// 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;
}
}
该函数检查msg.sender
是否有足够的余额来提取_amount
的以太币。
它通过一个低级别的 call
函数来发送请求的 _amount
,该函数将使用所有剩余的 "Gas" 来执行该操作
老实说,我不知道if
语句里面的代码是做什么的 :D 这是一种老式的代码,可能在Solidity 8.0中已经不能使用了。
它更新了msg.sender
的余额,减少了金额。
我可以看到这里有两个大问题!
该合约使用的Solidity版本<8.0,这意味着每一个数学运算都可能遭受到下溢/溢出攻击。该合约也使用了SafeMath
来处理uint256
,例如在donate
函数中就不存在这个问题。但是在 withdraw
中,当函数更新发送者的最终余额时,他们没有使用它。不使用它的原因是,合约认为它知道(在正常情况下)不会出现下溢,因为有if (balists[msg.sender] >= _amount)
检查。
让我们记住这件事,看看另一个问题。
第二个问题是由于合约没有遵循检查-生效-交互模式(Checks-Effects-Interactions)而引入的。它是什么意思呢?直接引用Solidity文档中的话:
大多数函数会首先进行一些检查(谁调用了这个函数,参数是否在范围内,他们是否发送了...
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!