这是我关于Ethernuat靶场的一些做题记录以及自己的一些分析感悟。
题目预览
准备工作
进入靶场的题目之间,我们首先需要做一些准备工作。
1.如果你还没有Metamask, 安装 MetaMask browser extension (适用于桌面Chrome, Firefox, Brave 或者Opera). 设置好拓展应用,并且在其界面上方选择’Rinkeby test network’测试网络.
2.准备好编辑器,可用在线remix 进行编辑:https://remix.ethereum.org/
3.在rinkeby水龙头处获取测试ether,可在第一关第六步处得到网址
1.Hello Ethernaut
分析
第一关更多的是让玩家熟悉靶场的一些语句执行,相当于一个教程,我们根据题目指示往下走即可。
攻击
首先点击关卡下方的按钮,生成新的关卡实例。
实例生成之后打开浏览器控制台.根据题目指示,首先输入await contract,info(),随后根据得到的提示一步步往下做。
方法告诉我们如果知道密码则将密码提交到authenticate(),我们输入contract查看当前合约拥有的方法,可以看到其中存在password,我们查看他的password,并将密码提交给authenticate.
随后我们就可以提交该实例了
攻击完成,进入下一关。
2.Fallback
分析
源码:
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
import '@openzeppelin/contracts/math/SafeMath.sol';
contract Fallback {
using SafeMath for uint256;
mapping(address => uint) public contributions;
address payable public owner;
constructor() public {
owner = msg.sender;
contributions[msg.sender] = 1000 * (1 ether);
}
modifier onlyOwner {
require(
msg.sender == owner,
"caller is not the owner"
);
_;
}
function contribute() public payable {
require(msg.value < 0.001 ether);
contributions[msg.sender] += msg.value;
if(contributions[msg.sender] > contributions[owner]) {
owner = msg.sender;
}
}
function getContribution() public view returns (uint) {
return contributions[msg.sender];
}
function withdraw() public onlyOwner {
owner.transfer(address(this).balance);
}
receive() external payable {
require(msg.value > 0 && contributions[msg.sender] > 0);
owner = msg.sender;
}
}
这关要求我们将合约的余额减为0,并且拿到合约的所有权。
分析完合约我们可以发现,有两个地方可以更改合约的owner,一个在contribute函数中,一个在合约的接收函数中。
而构造函数中将owner的贡献设定为1000ether,我们通过大于owner贡献的方式得到所有权肯定就得不偿失了,所以我们把目光锁定在receive函数中。
receive中的require要求msg.value大于0并且msg.sender的贡献大于0,不难看出,只要先向合约中转一次钱,再次向合约中转钱则会通过require限制。
拿到所有权后我们即可直接调用withdraw函数取出合约的所有余额。
攻击
分析完后我们开始攻击,首先按照之前的步骤生成新实例,调用contribute函数向合约中转入1wei,再向合约中直接转入1Wei即可触发receive函数。可以看到owner已经变成我自己了。
调用withdraw即可将合约的余额全部转回自己的账户中。提交实例,关卡完成。
3.Fallout
分析
源码:
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
import '@openzeppelin/contracts/math/SafeMath.sol';
contract Fallout {
using SafeMath for uint256;
mapping (address => uint) allocations;
address payable public owner;
/* constructor */
function Fal1out() public payable {
owner = msg.sender;
allocations[owner] = msg.value;
}
modifier onlyOwner {
require(
msg.sender == owner,
"caller is not the owner"
);
_;
}
function allocate() public payable {
allocations[msg.sender] = allocations[msg.sender].add(msg.value);
}
function sendAllocation(address payable allocator) public {
require(allocations[allocator] > 0);
allocator.transfer(allocations[allocator]);
}
function collectAllocations() public onlyOwner {
msg.sender.transfer(address(this).balance);
}
function allocatorBalance(address allocator) public view returns (uint) {
return allocations[allocator];
}
}
这关同样要求我们拿到合约的所有权。
整个合约只有这里能够改变合约的owner,且没有做任何调用限制,我们直接调用即可。
攻击
同样在关卡下点击生成新实例,然后在控制台输入 await contract.Fal1out(),然后查看owner,我们发现owner已经变成自己了,提交实例关卡成功。
这道题看起来很白痴,但其实是有真实案例的,一家公司更改了自己的合约名却忘记了更改自己合约构造函数的名字,导致其他人可以随意使用构造函数更改owner,损失惨重。
4.CoinFlip
分析
源码:
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
import '@openzeppelin/contracts/math/SafeMath.sol';
contract CoinFlip {
using SafeMath for uint256;
uint256 public consecutiveWins;
uint256 lastHash;
uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
constructor() public {
consecutiveWins = 0;
}
function flip(bool _guess) public returns (bool) {
uint256 blockValue = uint256(blockhash(block.number.sub(1)));
if (lastHash == blockValue) {
revert();
}
lastHash = blockValue;
uint256 coinFlip = blockValue.div(FACTOR);
bool side = coinFlip == 1 ? true : false;
if (side == _guess) {
consecutiveWins++;
return true;
} else {
consecutiveWins = 0;
return false;
}
}
}
这关要求我们连续猜中硬币的正反面,我们分析合约即可发现,硬币的正反面由区块高度决定,而我们知道,合约之间是可以调用的,且这次调用与初始调用存在于同一区块中,那我们就可以通过同样的方法来构造我们的攻击合约。
攻击
攻击合约:
contract coin {
CoinFlip intence;
uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
function setinterface(address addres) public {
intence = CoinFlip(addres);
}
function attack() public{
uint256 blockValue = uint256(blockhash(block.number - 1));
uint256 coinFlip = blockValue/(FACTOR);
bool side = coinFlip == 1 ? true : false;
intence.flip(side);
}
}
其中coin就是我们的攻击合约,我们通过与源码同样的方法构造出coinFlip,并判断正反,再传入目标合约进行攻击。
我们按同样的步骤生成新实例,得到目标合约地址后,打开remix编辑器,创建文件并将源码复制过去进行编译,点击部署栏,环境处选择metamask,选择攻击合约进行部署。
部署完成后,在函数setInterface参数中传入生成的实例地址,点击执行,随后点击attack进行攻击。
这样即为攻击成功一次,我们回到靶场查看合约的成功次数
已成功一次,接下来只要再攻击九次即可。我们依次调用十次后,再查看成功次数,发现已经十次,可以进行提交。
提交实例,关卡完成。
5.Telephone
分析
源码:
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
contract Telephone {
address public owner;
constructor() public {
owner = msg.sender;
}
function changeOwner(address _owner) public {
if (tx.origin != msg.sender) {
owner = _owner;
}
}
}
这关也要求我们拿到合约的所有权,不难发现只要满足changeOwner的调用条件即可完成。
要完成此条件我们就需要知道tx.origin与msg.sender的区别。
- tx.origin会遍历整个调用栈并返回最初发送调用(或交易)的帐户的地址。
- msg.sender为直接调用智能合约功能的帐户或智能合约的地址
两者的区别在于,如果同一笔交易中存在多次调用,则tx.origin不会改变,而msg.sender会发生变化。我们以此来构建我们的攻击合约。
攻击
攻击合约:
contract attack{
address public owner;
Telephone telephone;
constructor()public{
owner = msg.sender;
}
function changeAddress(address _contractAddr)public{
telephone=Telephone(_contractAddr);
}
function attacking()public{
telephone.changeOwner(msg.sender);
}
}
我们在攻击合约中调用目标合约的函数,则tx.origin仍然是我们自己的账户,但msg.sender会变为攻击合约的地址。
我们在靶场生成新实例,再在remix上对攻击合约进行编译,在部署时传入生成实例的地址,部署完成后,点击attacking攻击,回到控制台查看实例合约的owner,已经变为我们,即可提交实例.
提交实例,关卡完成。
6.Token
分析
源码:
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
contract Token {
mapping(address => uint) balances;
uint public totalSupply;
constructor(uint _initialSupply) public {
balances[msg.sender] = totalSupply = _initialSupply;
}
function transfer(address _to, uint _value) public returns (bool) {
require(balances[msg.sender] - _value >= 0);
balances[msg.sender] -= _value;
balances[_to] += _value;
return true;
}
function balanceOf(address _owner) public view returns (uint balance) {
return balances[_owner];
}
}
这关我们拥有初始的20个token,合约需要我们拿到更多的token,越多越好。
我们知道,如果要增加我们的余额数量,我们就需要从transfer函数入手,我们很容易可以发现合约并没有对uint进行溢出处理,我们先了解一下溢出是什么。
- 在solidity中,uint类型会存在溢出,例如unit8的最大值就是255,最小值为0;
- 上溢:如果在某个uint8变量的值在255时再次对他进行+1的操作,就会导致上溢,值变为0;
- 下溢:在某个uint8变量值为0时,对他进行减法操作,就会导致下溢,值变为255.
了解完溢出,我们就可以进行攻击了。
攻击
根据之前的步骤生成关卡新实例,然后我们直接向合约传入21个token,由于我们的余额只有20个,就会发生下溢,导致余额变成2的255次方.
,查看余额,发现余额已变,即可提交实例。
提交实例,关卡完成。