准备
随着区块链技术的逐渐推广,区块链安全也逐渐成为研究的热点。在其中,又以智能智能合约安全最为突出。Ethernaut正是入门研究区块链智能合约安全的好工具。
- 首先,应确保安装Metamask,如果可以使用Google Extension可以直接安装,否则可以使用FireFox安装
- 新建账号,并连接到RinkeBy Test Network(需要在Setting - Advanced里启用Show test networks,并在网络中进行切换)
- 访问Faucet并获取测试币,每天都有0.1Eth的额度
现在就可以开始Ethernaut的探索之旅了!
0. Hello Ethernaut
本节比较简单,所以我将更关注整体过程,介绍Ethernaut的实例创建等等,自己也梳理一下,所以会更详细一些。
准备工作
进入Hello Ethernaut,会自动提示连接Metamask钱包,连接后,示意图如下:
按F12打开开发者工具,在console界面就可以进行智能合约的交互。
创建实例并分析
单击 Get New Instance 以创建新的合约实例。
可以看出我们实际上是通过与合约0xD991431D8b033ddCb84dAD257f4821E9d5b38C33
交互以创建实例。在辅导参数中,调用0xdfc86b17
方法,附带地址为0x4e73b858fd5d7a5fc1c3455061de52a53f35d966
作为参数。实际上,所有关卡创建实例时都会向0xD991431D8b033ddCb84dAD257f4821E9d5b38C33
,附带的地址则是用来表明所处的关卡,如本例URL地址也为https://ethernaut.openzeppelin.com/level/0x4E73b858fD5D7A5fc1c3455061dE52a53F35d966
。
进入交易详情,查看内部交易,发现合约之间产生了调用。第一笔是由主合约调用关卡合约,第二笔是由关卡合约创建合约实例,其中实例地址为0x87DeA53b8cbF340FAa77C833B92612F49fE3B822
。
回到页面来看,可以确认生成实例的确为0x87DeA53b8cbF340FAa77C833B92612F49fE3B822
下面我们将进行合约的交互以完成本关卡。
合约交互
此时,在console界面可以通过player
和contract
分别查看用户当前账户和被创建合约实例。player
代表用户钱包账户地址,而contract
则包含合约实例abi
、address
、以及方法信息。
按照提示要求输入await contract.info()
,得到结果'You will find what you need in info1().'
。
输入await contract.info1()
,得到结果'Try info2(), but with "hello" as a parameter.'
。
输入await contract.info2('hello')
,得到结果'The property infoNum holds the number of the next info method to call.
。
输入await contract.infoNum()
,得到infoNum参数值为42
(Word中的首位)。这就是下一步要调用的函数(info42
)。
输入await contract.info42()
,得到结果'theMethodName is the name of the next method.
,即下一步应当调用theMethodName
。
输入await contract.theMethodName()
,得到结果'The method name is method7123949.
。
输入await contract.method7123949()
,得到结果'If you know the password, submit it to authenticate().
。
所以通过password()
可以获取密码ethernaut0
,并将其提交到authenticate(string)
。
注意当在进行authenticate()
函数时,Metamask会弹出交易确认,这是因为该函数改变了合约内部的状态(以实现对关卡成功的检查工作),而其他先前调用的函数却没有(为View)。
此时,本关卡已经完成。可以选择Sumbit Instance进行提交,同样要签名完成交易
总结
本题比较简单,更多的是要熟悉ethernaut的操作和原理。
1. Fallback
创建实例并分析
根据先前的步骤,创建合约实例,其合约地址为0xe0D053252d87F16F7f080E545ef2F3C157EA8d0E
。
本关卡要求获得合约的所有权并清空余额。
观察其源代码,找到合约所有权变更的入口。找到两个,分别是contribute()
及receive()
,其代码如下:
function contribute() public payable {
require(msg.value < 0.001 ether);
contributions[msg.sender] += msg.value;
if(contributions[msg.sender] > contributions[owner]) {
owner = msg.sender;
}
}
receive() external payable {
require(msg.value > 0 && contributions[msg.sender] > 0);
owner = msg.sender;
}
按照contribute()
的逻辑,当用户随调用发送小于0.001 ether
,其总贡献额超过了owner
,即可获得合约的所有权。这个过程看似简单,但是通过以下constructor()函数可以看出,在创建时,owner
的创建额为1000 ether
,所以这种方法不是很实用。
constructor() public {
owner = msg.sender;
contributions[msg.sender] = 1000 * (1 ether);
}
再考虑receive()
函数,根据其逻辑,当用户发送任意ether
,且在此之前已有贡献(已调用过contribute()
函数),即可获得合约所有权。receive()
类似于fallback()
,当用户发送代币但没有指定函数对应时(如sendTransaction()
),会调用该方法。
在获取所有权后,再调用withdraw
函数既可以清空合约余额。
合约交互
使用contract
命令,查看合约abi及对外函数情况。
调用await contract.contribute({value:1})
,向合约发送1单位Wei。
此时,调用await contract.getContribution()
查看用户贡献,发现贡献度为1,满足调用receiver()
默认函数的最低要求。
使用await contract.sendTransaction({value:1})
构造转账交易发送给合约,
调用await contract.owner() === player
确认合约所有者已经变更。
最后调用await contract.withdraw()
取出余额。
提交实例,显示关卡成功!
总结
本关卡也算比较简单,主要需要分析代码内部的逻辑,理解fallback()
及receive
的原理。