本文作者分享了通过Ethernaut平台上的Fallback挑战来学习Solidity智能合约安全的过程。文章详细解释了Fallback合约中contribute()和receive()函数的漏洞,以及如何利用这些漏洞来夺取合约所有权并提取合约中的所有资金。
在我之前的博客 通过黑客智能合约闯入 Web3 中,我介绍了 Ethernaut 并通过“Hello Ethernaut”挑战介绍了基础知识。如果你是智能合约的新手,我鼓励你查看一下。我分解了关键术语,并用图表解释了 Solidity 代码如何部署为链上合约。
现在,让我们进入有趣的部分:利用有缺陷的智能合约。Fallback 是 Ethernaut CTF 中的第二个级别,目标很简单:
就像在普通的 CTF 中一样,我从枚举开始。查看合约的 ABI,我找到了七个已定义的函数:
Fallback 的 ABI
借助 Ethernaut 的白盒 CTF 风格,此级别为我们提供了 Solidity 源代码。仔细查看 Solidity 代码,withdraw() 函数受到 onlyOwner 修饰符的保护,这意味着它只能由合约的所有者执行。目前,所有者设置为预定义的地址。为了赢得挑战,我们需要:
更多关于 onlyOwner 修饰符: https://learnblockchain.cn/article/20691
作为 Solidity 的初学者(以及发现其中缺陷),我立即想到了一个大问题:
“什么是 msg?”
msg 是 EVM 为每个合约调用或交易提供的全局对象。它包含有关当前调用的元数据。一些最常见的字段是:
简而言之,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() 函数耗尽帐户余额,必须拥有智能合约的所有权。
function contribute() public payable {
require(msg.value < 0.001 ether);
contributions[msg.sender] += msg.value;
if (contributions[msg.sender] > contributions[owner]) {
owner = msg.sender;
}
}
用简单的文字来说,以下是此代码逐行执行的操作:
这就是漏洞:所有权可以根据 contributions 来更改,这为滥用打开了大门。
receive() external payable {
require(msg.value > 0 && contributions[msg.sender] > 0);
owner = msg.sender;
}
receive() 是一个特殊函数,每当通过 sendTransaction() 将 Ether 发送到合约时,该函数都会运行。如果没有任何数据(sendTransaction 的可选参数)附加到交易,则 receive() 处理普通的 ETH 转账。这是一个关于在接收 Ether 时 receive() 和 fallback() 如何工作的 可靠解释。
用简单的文字来说,以下是 receive() 的作用:
这是有问题的,因为即使贡献过一次的任何人都可以稍后发送 Ether 以触发 receive(),从而使他们最终可以获得智能合约的所有权。
首先直接将 ETH 发送到合约是行不通的,因为我们的钱包地址尚未在 contributions 映射中(这是 receive() 函数的要求)。将我们的地址添加到此映射中的唯一方法是通过 contribute() 函数。
由于某些条件,直接对智能合约进行交易失败
当我们浏览其余的分析时,请记住这两个概念:
有两种可能的方法可以获取合约的所有权,每种方法都有其自身的缺点:
如果我们的 contribution 超过了当前所有者的 contribution,所有权就会发生变化。当前所有者拥有 1,000 ETH,而我们每次 contribution 仅限于 0.001 ETH (require(msg.value < 0.001 ether))。实际上,这条路径需要一辈子的时间才能超过所有者的 contribution。
首先,我们必须调用 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 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!