SC Reentrancy

重入攻擊(Reentrancy Attack)是一種智能合約中的漏洞,當合約在更新自身狀態(如餘額)之前,對外部合約進行調用時,攻擊者可能利用這一點重複調用原始函數,從而執行不當操作,例如多次提領資金。這種攻擊通常發生在合約向外部地址發送以太幣(Ether)或調用外部合約時,攻擊者可通過惡意合約重新進入(re-enter)原始合約,利用未更新的狀態進行惡意操作,最終可能耗盡合約中的所有資金。

範例(存在漏洞的合約)

以下是一個易受重入攻擊的智能合約範例:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Solidity_Reentrancy {
    mapping(address => uint) public balances;

    function deposit() external payable {
        balances[msg.sender] += msg.value;
    }

    function withdraw() external {
        uint amount = balances[msg.sender];
        require(amount > 0, "Insufficient balance");

        // Vulnerability: Ether is sent before updating the user's balance, allowing reentrancy.
        (bool success, ) = msg.sender.call{value: amount}("");
        require(success, "Transfer failed");

        // Update balance after sending Ether
        balances[msg.sender] = 0;
    }
}

漏洞分析

  • withdraw 函數
    • 在發送以太幣(msg.sender.call)後才將用戶餘額設置為 0。
    • 如果 msg.sender 是一個惡意合約,該合約可以在接收以太幣時立即重新調用 withdraw 函數。
    • 因為餘額尚未更新(仍為非零),攻擊者可以重複提領資金,直到耗盡合約的以太幣。
  • 問題:這種「先發送、後更新」的邏輯允許攻擊者在狀態更新前多次執行提領操作。

惡意攻擊合約範例

contract Attack {
    Solidity_Reentrancy victim;

    constructor(address _victim) {
        victim = Solidity_Reentrancy(_victim);
    }

    receive() external payable {
        victim.withdraw(); 
    }

    function attack() external {
        victim.deposit{value: 1 ether}(); 
        victim.withdraw(); 
    }
}

惡意攻擊合約運作過程:

假設 victim 漏洞合約總共有 10 ETH 的資金,這些資金來自多個用戶

  1. 攻擊的第一步:attack() 函式
    • victim.deposit{value: 1 ether}();:攻擊者先向有漏洞的 victim 合約存入 1 Ether。此時,victim 漏洞合約會記錄攻擊者的餘額為 1 Ether。
    • victim.withdraw();:接著,攻擊者觸發第一次提款。
    • 漏洞合約的內部狀態如下:
      • victim 漏洞合約總資金:10 ETH
      • Balances[attacker]:1 ETH (攻擊者的餘額)
  2. 漏洞觸發:victim.withdraw() 函式的執行
    • 檢查餘額:有漏洞的 withdraw 函式會檢查攻擊者的餘額是否足夠(require(userBalances[msg.sender] >= amount))。此時,餘額是 1 Ether,檢查通過。
    • 轉帳(外部呼叫):接著,withdraw 函式會執行 msg.sender.call{value: amount}(""),將 1 Ether 轉帳給攻擊者。
    • 漏洞點此時,victim 存在漏洞的合約還沒有將攻擊者的餘額清零或減少!
    • 漏洞合約的內部狀態如下:
      • victim 漏洞合約總資金:9 ETH
      • Balances[attacker]:1 ETH (攻擊者的餘額),漏洞合約還沒有將攻擊者的餘額清零或減少!
  3. 重入點:receive() 函式
    • victim 漏洞合約將 1 Ether 轉給攻擊合約時,攻擊合約的 receive() 函式會自動被觸發執行
    • receive() 函式內部有一行關鍵程式碼:victim.withdraw();
    • 這行程式碼會讓攻擊合約再次呼叫 victim 漏洞合約的 withdraw 函式。
  4. 循環攻擊
    • 當第二次進入 victim.withdraw() 時,因為存在漏洞合約的狀態還沒有被更新,它依然認為攻擊者的餘額是 1 Ether
    • 檢查餘額通過,victim 存在漏洞合約再次向攻擊合約轉帳 1 Ether。
      • 漏洞合約的內部狀態如下:
        • victim 漏洞合約總資金:8 ETH
        • Balances[attacker]:1 ETH (攻擊者的餘額),漏洞合約還沒有將攻擊者的餘額清零或減少
    • 攻擊合約的 receive() 函式再次被觸發,再次呼叫 withdraw
    • 這個過程會一直重複,直到 victim 存在漏洞合約內的 Ether 被耗盡。
      • 漏洞合約的內部狀態如下:
        • victim 漏洞合約總資金:0 ETH
        • Balances[attacker]:1 ETH (攻擊者的餘額),漏洞合約還沒有將攻擊者的餘額清零或減少

影響

重入攻擊可能導致以下嚴重後果:

  1. 資金耗盡:攻擊者可通過重複提領耗盡合約中的所有資金。
  2. 未授權操作:攻擊者可能觸發未經授權的功能調用,導致合約執行非預期的行為。
  3. 系統破壞:攻擊可能影響與合約相關的系統,造成更廣泛的損害。

修復方法

為防止重入攻擊,開發者應採取以下措施:

  1. 狀態優先更新:在調用外部合約或發送以太幣之前,完成所有內部狀態的更新(例如,將餘額設為 0)。
  2. 使用防重入保護:採用函數修飾符(如 OpenZeppelin 的 ReentrancyGuard)來防止重入攻擊,確保函數在執行期間不會被重複調用。
  3. 謹慎使用外部調用:盡量減少對外部合約的調用,並確保調用安全。

修復後的範例合約

以下是修復了重入漏洞的合約範例:

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

contract Solidity_Reentrancy {
    mapping(address => uint) public balances;

    function deposit() external payable {
        balances[msg.sender] += msg.value;
    }

    function withdraw() external {
        uint amount = balances[msg.sender];
        require(amount > 0, "Insufficient balance");

        // Fix: Update the user's balance before sending Ether
        balances[msg.sender] = 0;

        // Then send Ether
        (bool success, ) = msg.sender.call{value: amount}("");
        require(success, "Transfer failed");
    }
}

修復內容

1.狀態更新優先

  • withdraw 函數中,先將用戶餘額 (balances[msg.sender]) 設為 0,然後才發送以太幣。
  • 這確保即使攻擊者重新調用 withdraw,餘額已經為 0,無法再次提領。

2.安全性提升

  • 這種「先更新、後發送」的順序杜絕了重入攻擊的可能性。

總結

重入攻擊是智能合約中最危險的漏洞之一,可能導致資金被耗盡或合約功能被濫用。通過在外部調用前更新狀態、採用防重入保護機制(如 ReentrancyGuard)以及謹慎設計外部調用,開發者可以有效防止重入攻擊。修復後的合約展示了如何通過調整狀態更新順序來確保安全性,保護合約和用戶的資金。