深入解析Ethernaut CTF挑战:智能合约漏洞实战
上周Zeppelin发布了他们的以太坊CTF项目Ethernaut。这个CTF是了解如何与区块链交互和学习智能合约漏洞基础的良好入门。CTF托管在ropsten区块链上,你可以获得免费以太币。浏览器开发者控制台和metamask插件用于与CTF交互。
我有幸成为第一个完成所有挑战的人。以下是我的解决方案。
1. Fallback
这个挑战是第一个,我认为它更像是一个介绍,确保每个人都能使用API。让我们详细看看Solidity智能合约的样子。
挑战描述
合约由一个构造函数和四个函数组成。目标是成为合约的所有者并提取所有资金。
第一个函数是合约的构造函数,因为它与合约同名。构造函数是一个特定函数,仅在合约首次部署时调用一次,之后无法调用。此函数通常用于设置一些参数(这里是所有者的初始贡献)。
|
|
第二个函数contribute()将调用者发送的以太币数量(msg.value)存储在contributions映射中。如果此值大于合约所有者的贡献,则调用者成为合约的所有者。
|
|
getContribution()是一个简单的getter:
|
|
withdraw()允许合约所有者提取所有资金。注意函数签名后的onlyOwner关键字。这是一个修饰符,确保此函数仅由所有者调用。
|
|
最后,最后一个函数是合约的fallback函数。如果调用者之前做出了贡献,则可以执行此函数。
|
|
Fallback函数
要理解什么是fallback,我们必须了解以太坊中的函数选择器和参数机制。当你在以太坊中调用函数时,你实际上是在向网络发送交易。此交易包含发送的以太币数量(msg.value)和所谓的data,这是一个字节数组。此字节数组保存要调用的函数的id和函数的参数。他们选择使用函数签名的keccak256的前四个字节作为函数id。例如,如果函数签名是transfer(address,uint256),函数id是0xa9059cbb。
如果你想调用transfer(0x41414141, 0x42),数据将是:
|
|
在执行过程中,智能合约做的第一件事是使用调度器检查函数id。如果没有匹配,则调用fallback函数(如果存在)。
你可以使用我们的开源反汇编器Ethersplay可视化此调度器:
Ethersplay显示EVM调度器结构
(*) 为简化起见,从源代码中删除了Owner继承。你可以在此处找到solidity文件和运行时字节码:fallback
解决方案
如果我们将所有内容放在一起,我们必须:
- 调用contribution以在contributions中放入一些初始值
- 调用fallback函数成为合约的所有者
- 调用withdraw以获取所有资金
(1) 通过在浏览器的开发者工具控制台中调用contract.contribution({value:1})轻松完成。调用fallback函数(2)的一种简单方法是使用metamask插件直接向合约发送以太币。然后通过调用contract.withdraw()实现(3)。
2. Fallout
挑战描述
这里的目的是成为合约的所有者。
起初,这个合约似乎有一个构造函数和四个函数。但如果我们仔细查看构造函数,我们会发现函数的名称与合约的名称略有不同:
|
|
因此,此函数不是构造函数,而是一个经典的公共函数。合约部署后,任何人都可以调用它!
这可能看起来太简单而不像是真正的漏洞,但它是真实的。使用我们的内部静态分析器Slither,我们发现了几个犯此错误的合约(例如,ZiberCrowdsale或PeerBudsToken)!
解决方案
我们只需要调用contract.Fal1out()成为所有者,就是这样!
3. Token
挑战描述
这里我们在合约中获得了20个免费代币。我们的目标是找到一种持有大量代币的方法。
称为transfer的函数允许在用户之间转移代币:
|
|
起初,函数看起来很好,因为它似乎检查溢出
|
|
通常,当我必须处理算术计算时,我选择简单的方法,使用我们的开源符号执行器manticore来检查是否可以滥用合约。我们最近添加了对evm的支持,这是一个用于审计整数相关问题的非常强大的工具。但在这里,仔细一看,我们意识到_value和balances是无符号整数,意味着balances[msg.sender] - _value >= 0总是为真!
所以我们可以在以下产生下溢:
|
|
结果,balances[msg.sender]将包含一个非常大的数字!
解决方案
要触发下溢,我们可以简单地调用contract.transfer(0x0, 21)。然后balances[msg.sender]将包含2**256 – 1。
4. Delegation
挑战描述
这里的目的是成为合约Delegation的所有者。
在此合约中没有直接更改所有者的方法。但是,它持有另一个合约Delegate,具有此函数:
|
|
Delegation的一个特点是在fallback函数中使用delegatecall。
|
|
挑战的提示非常明确:
delegatecall的使用特别危险,并已在多个历史性黑客攻击中用作攻击向量。使用它,你的合约实际上是在说“这里,-其他合约-或-其他库-,随意处理我的状态”。委托者完全访问你的合约状态。delegatecall函数是一个强大的功能,但也是一个危险的功能,必须极其小心地使用。
请参阅The Parity Wallet Hack Explained文章,了解此想法如何被用于窃取3000万美元的准确解释。
所以这里我们需要调用fallback函数,并在msg.data中放入pwn()的签名,以便delegatecall将在Delegation的状态内执行函数pwn()并更改合约的所有者。
解决方案
正如我们在“Fallback”中看到的,我们必须在msg.data中放入pwn()的函数id;即0xdd365b8b。结果,Delegate.pwn()将在Delegation的状态内被调用,我们将成为合约的所有者。
5. Force
挑战描述
这里我们必须向一个空合约发送以太币。由于没有可支付的fallback函数,直接向合约发送以太币将失败。
有其他方法可以向合约发送以太币而不执行其代码:
- 调用selfdestruct(address)
- 指定地址作为挖矿奖励目的地
- 在合约创建之前向地址发送以太币
解决方案
我们可以创建一个合约,该合约将简单地调用selfdestruct(address)到目标合约。
|
|
注意我们使用可支付的构造函数。这样做,我们可以在构造时直接将一些值放入合约中。然后此值将通过selfdestruct发送。
你可以使用Remix浏览器在ropsten上轻松测试和部署此合约。
6. Re-entrancy
挑战描述
最后一个挑战!你必须在合约创建期间向合约发送一个以太币,然后取回你的钱。
合约有四个函数。我们对其中两个感兴趣。
donate让你向合约捐赠以太币,发送的以太币数量存储在balances中。
|
|
withdraw,第二个函数允许用户检索存储在balances中的以太币。
|
|
以太币通过调用msg.sender.call.value(_amount)()发送。
起初,这里一切看起来都很好,因为发送的值在balances[msg.sender] -= _amount;中减少,并且没有办法在不发送以太币的情况下增加balances。
现在回想一下“Fallback”中解释的fallback函数机制。如果你向包含fallback函数的合约发送以太币,此函数将被执行。这里的问题是什么?你可以有一个fallback函数回调withdraw,因此msg.sender.call.value(_amount)()可以在balances[msg.sender] -= _amount执行之前执行两次!
此漏洞称为重入漏洞,并在臭名昭著的DAO黑客攻击中使用。
解决方案
要利用重入漏洞,你必须使用另一个合约作为代理。此合约将需要:
- 有一个将调用withdraw的fallback函数
- 调用donate以在易受攻击的合约中存入以太币
- 调用withdraw
在我们的not-so-smart-contracts数据库中,你将找到一个利用此漏洞的通用骨架示例。我将把适应此骨架的练习留给读者。
与之前的挑战类似,你可以使用Remix浏览器在ropsten上测试和部署合约。
结论
这个CTF真的很酷。界面使得很容易进入智能合约安全。Zeppelin做得很好,所以感谢他们!
如果你对文章中提到的工具感兴趣,或者你需要智能合约安全评估,请随时联系我们!
如果你喜欢这篇文章,请分享: Twitter LinkedIn GitHub Mastodon Hacker News