我的第一次链上黑客攻击:Fallback

本文作者分享了通过Ethernaut平台上的Fallback挑战来学习Solidity智能合约安全的过程。文章详细解释了Fallback合约中contribute()和receive()函数的漏洞,以及如何利用这些漏洞来夺取合约所有权并提取合约中的所有资金。

我的第一次链上黑客行为:Fallback

破解 Solidity 智能合约逻辑的分析

在我之前的博客 通过黑客智能合约闯入 Web3 中,我介绍了 Ethernaut 并通过“Hello Ethernaut”挑战介绍了基础知识。如果你是智能合约的新手,我鼓励你查看一下。我分解了关键术语,并用图表解释了 Solidity 代码如何部署为链上合约。

现在,让我们进入有趣的部分:利用有缺陷的智能合约。Fallback 是 Ethernaut CTF 中的第二个级别,目标很简单:

  1. 声明合约的所有权。
  2. 将其余额减少到 0。

就像在普通的 CTF 中一样,我从枚举开始。查看合约的 ABI,我找到了七个已定义的函数:

Fallback 的 ABI

借助 Ethernaut 的白盒 CTF 风格,此级别为我们提供了 Solidity 源代码。仔细查看 Solidity 代码,withdraw() 函数受到 onlyOwner 修饰符的保护,这意味着它只能由合约的所有者执行。目前,所有者设置为预定义的地址。为了赢得挑战,我们需要:

  1. 将合约的所有者更改为我们自己的地址。
  2. 然后调用 withdraw() 来清空合约的余额。

更多关于 onlyOwner 修饰符: https://learnblockchain.cn/article/20691

背景知识:msg 对象

作为 Solidity 的初学者(以及发现其中缺陷),我立即想到了一个大问题:

什么是 msg?”

msg 是 EVM 为每个合约调用或交易提供的全局对象。它包含有关当前调用的元数据。一些最常见的字段是:

  • msg.sender:发起调用的地址
  • msg.value:随交易发送的 ETH 金额(以 wei 为单位)
  • msg.data:原始 calldata
  • msg.sig:msg.data 的前四个字节(函数选择器)

简而言之,msg 是合约如何知道谁调用了它、他们发送了什么数据以及附加了多少 ETH。

更多关于 msg 对象: https://suyashblogs.hashnode.dev/msg-in-solidity https://medium.com/upstate-interactive/what-you-need-to-know-about-msg-global-variables-in-solidity-566f1e83cc69

存在漏洞的函数

在智能合约实现中,有两个函数因容易被滥用而脱颖而出:contribute()receive()。这些函数可能存在漏洞,因为它们可以更改合约的所有权。回想一下,要使用 withdraw() 函数耗尽帐户余额,必须拥有智能合约的所有权。

contribute()

function contribute() public payable {
  require(msg.value < 0.001 ether);
  contributions[msg.sender] += msg.value;
  if (contributions[msg.sender] > contributions[owner]) {
    owner = msg.sender;
  }
}

用简单的文字来说,以下是此代码逐行执行的操作:

  1. 调用者必须附加少于 0.001 ETH 才能运行 contribute()
  2. 如果满足条件 #1,则合约会将 ETH 金额添加到调用者在 contributions 映射中的记录。
  3. 如果调用者的总 contributions 现在大于当前所有者的。
  4. 合约会将 owner 变量更新为调用者的地址,从而有效地使他们成为新的所有者。

这就是漏洞:所有权可以根据 contributions 来更改,这为滥用打开了大门。

receive()

receive() external payable {
  require(msg.value > 0 && contributions[msg.sender] > 0);
  owner = msg.sender;
}

receive() 是一个特殊函数,每当通过 sendTransaction() 将 Ether 发送到合约时,该函数都会运行。如果没有任何数据(sendTransaction 的可选参数)附加到交易,则 receive() 处理普通的 ETH 转账。这是一个关于在接收 Ether 时 receive()fallback() 如何工作的 可靠解释

用简单的文字来说,以下是 receive() 的作用:

  1. 在合约可以接受 ETH 之前,它会检查两个前提条件:发送的 ETH 数量 (msg.value) 必须大于 0;发送者必须已经至少进行了一次 contribution (contributions[msg.sender] > 0)。
  2. 如果满足这两个条件,则合约会将发送者设置为新的所有者。

这是有问题的,因为即使贡献过一次的任何人都可以稍后发送 Ether 以触发 receive(),从而使他们最终可以获得智能合约的所有权。

滥用尝试:获取智能合约的所有权

首先直接将 ETH 发送到合约是行不通的,因为我们的钱包地址尚未在 contributions 映射中(这是 receive() 函数的要求)。将我们的地址添加到此映射中的唯一方法是通过 contribute() 函数。

由于某些条件,直接对智能合约进行交易失败

当我们浏览其余的分析时,请记住这两个概念:

  1. 没有附加数据:contract.sendTransaction({ value: 100 }) 触发 receive() 函数。
  2. 附加了数据:它会触发 fallback() 函数。

有两种可能的方法可以获取合约的所有权,每种方法都有其自身的缺点:

方法 #1:通过 contribute()

如果我们的 contribution 超过了当前所有者的 contribution,所有权就会发生变化。当前所有者拥有 1,000 ETH,而我们每次 contribution 仅限于 0.001 ETH (require(msg.value < 0.001 ether))。实际上,这条路径需要一辈子的时间才能超过所有者的 contribution。

方法 #2:通过 receive()

首先,我们必须调用 contribute() 一次,以将我们的地址包含在 contributions 映射中。之后,发送带有 sendTransaction() (≤ 0.001 ETH) 的 ETH 将满足 receive() 条件。一旦触发,它会将我们设置为新的所有者。

比较实际可行的方法是第二种:使用 contribute() 进入映射,然后利用 receive() 来劫持所有权。因此,我们将使用该方法并调用 sendTransaction() 函数。由于它以 wei 为单位处理 ETH 值,因此我们需要使用 toWei() 函数将我们的 ETH 转换为 wei。

将 wei 视为 ETH 的最小面额,类似于美分与美元的关系。以下是一些关于加密货币面额的有用资源:

https://www.gemini.com/cryptopedia/satoshi-value-gwei-to-ether-to-wei-converter-eth-gwei https://academy.binance.com/en/glossary/wei

这是我们的交易之后 contributions 映射的前后视图。请注意 words 数组是如何变化的。

成功交易后 words 数组值发生变化

一种更简洁的方法。

现在我们被列为 contributor,我们可以调用 sendTransaction() 来触发 receive() 函数。经检查,智能合约的所有者地址已更新为我们的地址!

智能合约所有者的地址现在设置为我们钱包的地址

最后一步:提取帐户的余额

现在我们已经获得了合约的所有权,我们可以调用 withdraw() 函数来耗尽其全部余额。完成后,我们将实例提交回 Ethernaut 以进行验证。

就这样,Fallback 就被解决了!

  • 原文链接: blog.blockmagnates.com/m...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
blockmagnates
blockmagnates
The New Crypto Publication on The Block