SC Proxy and upgradeability vulnerabilities

代理與可升級性漏洞是指當智能合約採用可升級架構,但其升級路徑、初始化機制或管理員控制權設計不良或配置錯誤時所產生的漏洞。攻擊者可以劫持代理管理員或升級角色,進而部署惡意的實作合約、重新初始化合約以奪取所有權,或是繞過初始化與遷移步驟中的關鍵檢查。

這類漏洞會影響所有使用可升級架構的合約類型,包括:

  • DeFi: 借貸協議(Lending)、金庫(Vaults)、去中心化交易所(DEXes)。
  • NFT: 項目方系列(Collections)、交易市場(Marketplaces)。
  • DAO: 治理系統(Governance)、金庫(Treasuries)。
  • 跨鏈橋(Bridges): 訊息傳遞器(Messengers)、資產合約(Asset Contracts)。
  • L2/跨鏈系統。

常見的升級模式包括透明代理(Transparent Proxy)、UUPS(EIP-1822)、信標代理(Beacon Proxy)以及自定義的「路由—實作(Router-Implementation)」設計。在非 EVM 鏈上(例如 Move 模組、Solana 的程序升級),也存在類似的升級機制,且同樣面臨著高度信任與初始化失效的風險。


代理模式

區塊鏈上的智慧合約一旦部署就無法修改(Immutable)。但專案需要更新功能或修復 Bug 怎麼辦?開發者於是發明了代理模式(Proxy Pattern)

你可以把這想像成開一家餐廳

  • 代理合約(Proxy): 這是「餐廳的店面(地址)」。它不負責做菜,只負責收錢、接單,並把客人的要求原封不動地傳給後廚。所有的資產(使用者的錢、代幣餘額)都存在店面(Proxy)裡。
  • 邏輯合約(Implementation / Logic): 這是「後廚的廚師」。它負責研發菜單(執行邏輯、加減乘除)。如果廚師生病或菜色要升級,專案方只需要換一個新廚師,但餐廳的地址(Proxy)永遠不變。

兩者之間溝通的橋樑叫做 delegatecall(委託調用)。這是一種特殊的程式碼調用方式:執行「新廚師」的程式碼,但扣錢和記帳都在「店面(Proxy)」的帳本上。


漏洞容易發生的地方

1. Upgrade and admin roles

  • 技術本質: 決定「誰能修改 Proxy 合約中指向 Implementation 的地址變數」。
  • 關注點:
    • 權限控制: 升級權限是否被嚴格限制(例如由多簽錢包或 DAO 治理合約控制)?如果升級函數暴露給公眾,任何人都能替換邏輯。
    • 存儲佈局相容性: 升級到新合約時,新合約的變數宣告順序必須與舊合約完全一致,且只能在末尾追加(Append-only)新變數。如果任意改變順序,會導致新程式碼讀取到錯誤的歷史數據。

2. Initialization and re-initialization

  • 技術本質: 代理架構中,Implementation 的 constructor 在部署時只會影響它自己的環境,無法改變 Proxy 的狀態。因此,Proxy 必須依賴一個普通的函數(通常叫 initialize())來設定初始狀態(如 Owner、代幣總量)。
  • 關注點:
    • Initialization Guard: 例如使用 OpenZeppelin 的 initializer 修飾符,確保該函數只能被呼叫一次
    • 再初始化(Re-initialization): 當合約需要大版本升級(例如 V1 升級到 V2)時,會用到OpenZeppelin 的 reinitializer(2)。如果這些升級初始化函數沒有做好版本控制或權限檢查,就會留下破綻。

3. Proxy delegation

  • 技術本質: 理解 delegatecall 的運作脈絡(Context)。
  • 關注點:
    • 當 A 合約 delegatecall B 合約時,程式碼雖然是 B 的,但執行環境(Storage、Balances、msg.sender、msg.value)全部都在 A 合約中。
    • 舉例: 使用者呼叫 Proxy,msg.sender 是使用者。Proxy 透過 delegatecall 呼叫 Implementation,此時在 Implementation 的程式碼裡,msg.sender 依然是使用者,而不是 Proxy。如果開發者誤以為 msg.sender 是 Proxy,就會導致權限邏輯大亂。

4. Storage layout

  • 技術本質: EVM 儲存資料是基於 Slot,每個 Slot 大小為 32 位元組,從 Slot 0、Slot 1 依序往下排。
  • 關注點:
    • 插槽衝突(Slot Collision): Proxy 合約本身通常需要儲存一個變數叫做 _implementation(記錄目前後端地址)。如果這個變數不小心跟 Implementation 合約的某個業務變數(例如 ownerbalance)佔用了同一個 Slot 編號,兩者就會互相覆蓋。

5. Timelocks and governance

  • 技術本質: 升級合約的「流程防禦機制」。
  • 關注點:
    • 時間鎖(Timelock): 限制管理員在發出升級提案後,必須等待 24~48 小時才能真正執行。這給了社群檢查新程式碼的時間,若發現專案方意圖不軌(Rug Pull)或程式碼有 Bug,使用者有時間撤資。
    • 回滾能力(Rollback Capability): 如果新升級的 V2 版本上線後一分鐘內被發現有嚴重漏洞,系統是否有權限或機制在第一時間快速「退回」到穩定的 V1 版本。

常見攻擊方法

1. Unprotected upgrade functions

  • 手法: 開發者在編寫 upgradeTo()upgradeToAndCall() 函數時,忘記加上 onlyOwner 或權限檢查修飾符。
  • 後果: 攻擊者直接調用該函數,傳入自己部署的惡意合約地址。Proxy 隨即指向黑客的合約,黑客直接寫一個 withdrawAll() 函數把 Proxy 裡儲存的所有資產(DeFi 資金池裡的錢)全部提走。

2. Re-initialization

  • 手法: 專案方在合約升級時,可能留下了一個用於 V2 初始化的函數(如 initializeV2()),但沒有正確限制只有 Admin 能呼叫,或者沒有綁定正確的初始化版本號。
  • 後果: 攻擊者強先呼叫這個函數。由於初始化函數會重設關鍵變數,攻擊者利用它直接覆蓋掉原本的 owner 地址,將合約所有權變更為自己,隨後掌控整個協議。

3. Initialization through delegatecall

  • 手法: 許多代理架構允許在升級的當下,順便透過 delegatecall 去執行新合約的初始化邏輯(即 upgradeToAndCall(impl, data))。
  • 後果: 如果這個入口沒鎖好,攻擊者可以傳入自定義的 data(惡意參數)。因為是以 delegatecall 執行,攻擊者傳入的參數可以直接竄改 Proxy 合約核心儲存區(Storage)的任何資料,包含直接改寫管理員權限或資產歸屬。

4. Storage collision leading to overwrites

  • 手法: 當 Proxy 合約與 Implementation 合約在 Slot 分配上發生重疊時(例如雙方都試圖寫入 Slot 0)。
  • 後果: 攻擊者透過正常業務邏輯去修改 Implementation 的變數(例如去註冊一個名稱,觸發合約修改 Slot 0 的資料)。然而在 Proxy 的視角裡,Slot 0 存的是 admin 的地址。攻擊者透過這種「隔山打牛」的方式,利用合約自身的邏輯把 Slot 0 改成了自己的地址,平白無故變成了合約的管理員。

這些問題(代理與升級漏洞)在技術本質上,往往跟『存取控制(Access Control)』是重疊的;但因為代理與升級機制出錯時,會對整個系統造成『毀滅性的連帶打擊(Systemic Impact)』,所以必須把它們拉出來,當作一個獨立的大類別來嚴肅對待。


漏洞範例

Vulnerable Upgradeable Proxy Admin

以下是一個存在漏洞的智能合約範例:

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

contract VulnerableProxyAdmin {
    address public admin;
    address public implementation;

    constructor(address _implementation) {
        // Critical: no way to set custom admin; implicitly trusts deployer logic
        admin = msg.sender;
        implementation = _implementation;
    }

    function upgrade(address newImplementation) external {
        // Missing: access control (only admin) and sanity checks
        implementation = newImplementation;
    }
}

漏洞分析

1.升級函數完全沒有權限控制(No access control on upgrade)

  • 預期邏輯: 本來開發者的意圖應該是「只有 admin(管理員)可以更換新廚師(Implementation)」。
  • 現實慘劇: 因為函數上只有一個 external(公開調用),完全沒有加上 require(msg.sender == admin, "Not admin"); 或者是 OpenZeppelin 的 onlyOwner 修飾符。
  • 駭客攻擊手法: 任何路人、甚至是黑客,都可以自己部署一個寫滿惡意邏輯的合約(例如:只要有人轉錢進來,就立刻把錢轉到黑客地址),然後直接呼叫這個 upgrade(malicious contract address)。從這一秒開始,這個代理系統就被黑客全面接管了。

2.完全沒有對新地址做安全檢查(No checks on newImplementation)

除了權限大開之外,這個函數對傳入的 newImplementation(新合約地址)採取「完全信任」的態度,這會引發以下幾種災難:

  • 零地址檢查(Non-zero address check): 如果有人不小心呼叫 upgrade(0x0000000000000000000000000000000000000000)(傳入空地址)。合約會直接指向一個不存在的地方。一旦指向零地址,後續所有透過這個 Proxy 進行的交易都會直接失敗,這等同於直接把合約給「變磚(Brick)」、資產永久鎖死。
  • 介面相容性檢查(Interface compatibility): 好的升級架構(例如 UUPS 模式中的 proxiableUUID() 檢查)會去驗證「新地址到底是不是一個真正相容的智慧合約」。如果隨便傳入一個普通的錢包地址(EOA),或者是功能完全不搭嘎的合約,Proxy 轉發過去後就會因為找不到對應的函數而直接崩潰。

Initialization & Re-Initialization Risks

以下是一個存在漏洞的智能合約範例:

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

contract VulnerableLogic {
    address public owner;

    // Missing initializer guard
    function initialize(address _owner) external {
        owner = _owner;
    }
}

漏洞分析

開發者的預期邏輯

開發者知道在可升級架構中,代理合約(Proxy)無法讀取實作合約(Implementation)的 constructor。所以他想:「那我就寫一個普通的函數叫 initialize,來手動當作建構函數使用。」

開發者預期的安全部署腳本(Script)是這樣的:

  • 步驟 A: 部署實作合約 VulnerableLogic
  • 步驟 B: 部署代理合約 Proxy,並將內部指針指向 VulnerableLogic
  • 步驟 C(關鍵): 部署完成後的第一秒鐘,開發者自己立刻發送一筆交易去呼叫 Proxy.initialize(owner address)

開發者天真地以為:「反正我部署完就『馬上』呼叫了,只要我手腳夠快把 owner 改成我,這家店以後就是我的了,路人就算想呼叫也只是幫我重新設定一次而已,沒差吧?」

攻擊手法

  • 劇本一:攔截初始化交易(Front-running 奪權)

區塊鏈是一個「黑暗森林」,駭客絕對不會跟開發者比手速,他們使用的是自動化監控機器人(Mempool Sniffer)與Front-running手段。

  1. 監控: 駭客的機器人在區塊鏈的「交易暫存區(Mempool)」盯著。當開發者執行到步驟 C,發出 initialize(owner address) 的交易時,駭客的機器人瞬間偵測到了。
  2. 強跑(Front-run): 駭客立刻發出一筆一模一樣的交易,呼叫 Proxy.initialize(hacker address),但是給出極高的手續費(Gas Fee)。
  3. 鳩佔鵲巢: 區塊鏈的礦工(驗證者)看到駭客給的錢比較多,便把駭客的交易排在前面。
  4. 慘劇: 駭客的交易先執行,代理合約(Proxy)底層的 owner 被寫成了駭客。一秒後,開發者的交易才執行,此時可能因為邏輯衝突失敗,或者開發者只是傻傻地把 owner 重新寫了一次(但此時合約內的最高管理員已經被定性為駭客的眼線了)。
  • 劇本二:直接攻擊「無人認領」的實作合約

如果開發者運氣好,步驟 C 成功了,代理合約(Proxy)裡的 owner 確實變成了開發者。但不代表這樣就安全,駭客會轉向去攻擊那個單獨躺在鏈上的「實作合約(VulnerableLogic)本身」。

  1. 漏洞: 雖然 Proxy 裡的 owner 被設定了,但是實作合約(VulnerableLogic)自己本體裡面的 owner 變數依然是空白的(0x000…)!
  2. 奪權: 駭客直接呼叫「實作合約」的地址,執行 initialize(hacker address)。這時候,駭客變成了實作合約本體的 owner
  3. 自殺攻擊(Self-destruct): 如果這個實作合約內部包含、或者未來升級包含了 selfdestruct(自毀)的邏輯。身為實作合約 Owner 的駭客,可以直接下達自毀指令,把實作合約本體的程式碼從區塊鏈上抹除。
  4. 結果: 代理合約(Proxy)頓時失去了指向的目標(指向了一片虛無)。使用者存放在 Proxy 裡面的幾千萬美元會永遠鎖死,再也無法提現或呼叫,整個協議直接報廢。

2025 Case Studies

Kinto 協議遭駭事件(2025 年 7 月,損失 155 萬美元)

攻擊者利用了未初始化的 ERC1967 代理合約。他們偵測到剛部署、但尚未被正確初始化的代理合約,隨後直接對其進行初始化,並指向包含「潛伏後門(Dormant Backdoors)」的惡意實作合約。幾個月後,攻擊者啟動了該後門,將代理合約升級為惡意程式碼,並直接鑄造(Mint)大量的 K 代幣,藉此捲走 155 萬美元。該漏洞的核心在於:未受保護的初始化機制,允許任何人都能直接成為該代理合約的管理員(Proxy Admin)。

全域未初始化代理連環攻擊(2025 年,全網協議損失超過 1,000 萬美元)

這是一場針對多條 EVM 相容鏈上、多個未初始化 ERC1967 代理合約所展開的大規模無差別攻擊活動。攻擊者利用自動化掃描器,專門在正統開發者來得及呼叫初始化之前,劫持並偵測剛部署好的代理合約,隨後用惡意的實作合約搶先完成初始化。這些埋入的暗門在合約中潛伏了數個月,成功規避了常規的程式碼審計(Audits)。當暗門被激活時,攻擊者便能直接升級這些代理合約並將裡面的資金全數掏空。