iT邦幫忙

0

資安攻防 (四):權限控制漏洞與 Ownable 實作缺陷

  • 分享至 

  • xImage
  •  

在 Web3 的世界裡,我們常說「程式碼就是法律」。過去幾天我們討論的重入攻擊與整數溢位,都屬於「邏輯與數學運算」上的漏洞。但現在要探討的是一種極度低級、卻屢見不鮮且破壞力驚人的失誤:權限控制漏洞。

想像你設計了一個金庫,裡面配備了最先進的雷射防盜網與指紋辨識系統。結果,你居然把「設定新指紋」的控制面板直接放在金庫外面的大馬路上,而且沒有上鎖!這就是權限控制漏洞的最佳寫照。今天,我們就來看看開發者是如何不小心把金庫大門敞開,以及我們該如何防禦。

致命的建構子與初始化函數陷阱
在實作 ERC20 代幣時,我們學過可以使用 Ownable 模組,透過 onlyOwner 修飾子來保護像是 mint 這種高風險的函數。這聽起來很安全,但在實務上,權限轉移的漏洞通常發生在合約初始化的階段。

歷史慘案:Rubixi 合約與建構子命名錯誤
在非常早期的 Solidity 版本(0.4.22 之前),合約的建構子(Constructor)並不是用 constructor() 關鍵字,而是用「與合約名稱完全相同的函數」來定義。

曾經有一個名為 DynamicPyramid 的龐氏騙局合約,開發者後來決定把專案改名為 Rubixi。但他改了合約名稱,卻忘記改建構子的名稱
'''
contract Rubixi {
address public owner;

// 開發者忘記把這個改叫 Rubixi()
function DynamicPyramid() public {
    owner = msg.sender;
}

}
'''
結果,這個原本應該只在部署時執行一次的建構子,變成了一個任何人都可以在部署後呼叫的普通 public 函數!駭客輕而易舉地呼叫了 DynamicPyramid(),把自己變成了合約的 owner,並把裡面的錢全部捲走。

現代陷阱:可升級合約 (Upgradable Contracts) 的 initialize
雖然現代 Solidity 已經解決了建構子命名的問題,但在目前的 DeFi 生態中,為了讓合約可以修補 Bug,我們大量採用了「代理合約 (Proxy)」架構。在這種架構下,不能使用原生的 constructor,而是必須寫一個 initialize 函數來設定初始變數。

漏洞實例:
'''
contract VulnerableVault {
address public admin;
bool public isInitialized;

// 致命錯誤:忘記加上權限控管或防止重複呼叫的機制
function initialize() public {
    admin = msg.sender;
    isInitialized = true;
}

function withdrawAll() public {
    require(msg.sender == admin, "Only admin can withdraw");
    // 提款邏輯...
}

}
'''
如果開發者在部署完代理合約後,忘記在第一時間呼叫 initialize(),駭客就可以寫一個機器人,搶先呼叫這個函數,直接奪取 admin 權限!即使開發者已經呼叫過了,如果沒有防止重複呼叫的機制,駭客依然可以再次呼叫,覆寫掉原本的管理者。

防禦最佳實踐:如何鎖好大門?
防禦這種漏洞的方式非常標準化,我們必須再次請出 OpenZeppelin 的工具庫:

  1. 使用 Initializable 模組
    對於需要使用 initialize 函數的可升級合約,絕對不能自己手刻布林值來判斷是否初始化過。應該繼承 OpenZeppelin 的 Initializable,並加上 initializer 修飾子。
    '''
    import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol";

contract SecureVault is Initializable {
address public admin;

// initializer 修飾子確保這個函數在合約生命週期中,永遠只能被成功呼叫「一次」
function initialize() public initializer {
    admin = msg.sender;
}

}
'''

  1. 嚴格審視每一個 public 與 external 函數
    身為資安稽核人員,當我們在審閱一份合約時,第一步絕對是把所有標記為 public 或 external 的函數挑出來。只要這些函數涉及到狀態變數的修改或資金轉移,就必須質問自己:「誰有權限呼叫它?」

如果它屬於敏感操作,就必須毫不猶豫地加上 onlyOwner,或是實作更精細的 Role-Based Access Control (RBAC)(例如將權限細分為 MINTER_ROLE、ADMIN_ROLE)。

我們看遍了合約內部的各種致命失誤。但 Web3 的資安挑戰遠不止於此。


圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言