這一關的核心旨在揭示 Solidity 中 private 修飾符的安全盲區。許多開發者誤以為將變數設為私有即可隱藏資訊,但實際上這只能限制其他合約的呼叫,完全無法改變區塊鏈底層資料全網公開的本質。這道題目帶給開發者最重要的資安教訓是,絕對不可將密碼、私鑰等任何敏感資料以明文形式寫入智能合約中,所有上鏈的資料都應視為完全公開。
題目位置:https://ethernaut.openzeppelin.com/level/8
題目說明
這關會給你一個名為 Vault 的智能合約實例(你可以把它想像成一個數位保險箱)。你的唯一目標是:解鎖這個保險箱。 具體來說,就是想辦法將合約裡面的狀態變數 locked 從預設的 true(上鎖)變成 false(解鎖)。
合約內容如下:
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Vault {
bool public locked;
bytes32 private password;
constructor(bytes32 _password) {
locked = true;
password = _password;
}
function unlock(bytes32 _password) public {
if (password == _password) {
locked = false;
}
}
}
代碼解說
1. 狀態變數 (State Variables)
這是合約用來儲存資料的地方,資料會永久記錄在區塊鏈上。
bool public locked;:宣告一個布林值(只有 true 或 false),用來代表保險箱的開關狀態。設定為public,所以任何人都可以隨時查看它現在是不是鎖著的。bytes32 private password;:宣告一個 32 bytes 的變數來存放密碼。作者刻意加上private(私有)。
2. 建構子 (Constructor)
constructor(bytes32 _password):這段程式碼只會在合約「誕生(部署到區塊鏈上)」的那一瞬間執行一次,之後就永遠不能再被呼叫。- 部署這個合約的人,會在部署時傳入一組自己設定的密碼(
_password)。 - 合約一建立,就會立刻把
locked設為true(上鎖),並把部署者傳入的密碼存進底層的password變數中。
3. 解鎖函數 (Unlock Function)
function unlock(bytes32 _password) public:這是一個公開的方法,任何人都可以呼叫它,並嘗試輸入密碼來開鎖。if (password == _password):合約會拿你剛剛輸入的密碼(_password),去跟當初存在區塊鏈底層的密碼(password)做比對。locked = false;:如果兩者完全一樣,條件成立,保險箱狀態就會被改成false(解鎖)。這就是你剛才成功觸發的最後一步。
漏洞分析
當你將變數設為 private,這只是 Solidity 編譯器層級的保護機制。它唯一的作用是:禁止其他智能合約透過合約程式碼直接呼叫並讀取這個變數。 例如,合約 B 無法寫出 合約A.password() 這樣的程式碼來獲取密碼,編譯器在編譯階段就會直接報錯阻止。
以太坊是一個公開的帳本(Public Ledger)。所有的交易紀錄、智能合約的程式碼,以及合約當下的所有狀態(State Variables),都必須公開同步並儲存在每一個以太坊節點的硬碟中。 因為資料就存在所有節點的公開資料庫裡,任何人都可以繞過智能合約的程式碼限制,直接向區塊鏈節點查詢「特定記憶體位置(Storage Slot)」裡存放的原始資料。
以太坊智能合約的儲存槽(Storage Slot)是依序排列的,每個槽的容量為 32 bytes(256 bits):
- Slot 0: 儲存了第一個變數
locked。 - Slot 1: 儲存了第二個變數
password。即使它被標示為private,這些位元組依然實實在在地寫入並儲存在區塊鏈的第 1 號儲存槽中。
解答步驟
1.讀取密碼
只需要打開瀏覽器的開發者控制台,使用 Web3.js 提供的區塊鏈底層 API getStorageAt,直接讀取合約地址上的第 1 號儲存槽:
> web3.eth.getStorageAt(instance, 1).then(web3.utils.toAscii)
...omit...
A very strong secret password :)
...omit...
> web3.eth.getStorageAt(instance, 1, console.log)
...omit...
0x412076657279207374726f6e67207365637265742070617373776f7264203a29
...omit...
補充說明:其實 web3.eth.getStorageAt 只是 Web3.js 幫你包裝好的語法。在以太坊節點底層的 JSON-RPC 協議中,本來就有一個公開的標準方法叫做 eth_getStorageAt。去翻閱 Web3.js 的 API 手冊找尋對應的功能,就可以找到。
2.將密碼填入
> await contract.unlock("0x412076657279207374726f6e67207365637265742070617373776f7264203a29")
3.檢查密碼是否正確
執行以下指令,如果是 false 表示成功
> await contract.locked()
false
補充說明:雖然原始碼裡沒有看到 function locked(),但其實 Solidity 編譯器自動幫你加上去了。在 Solidity 語言中,有一個非常重要的特性:只要你將狀態變數宣告為 public,編譯器就會在背後自動為這個變數生成一個同名的「讀取函數(Getter Function)」。
換句話說, 當合約中有 bool public locked; ,編譯器在編譯成位元糕(Bytecode)時,會偷偷幫合約加上類似以下的程式碼
function locked() public view returns (bool) {
return locked;
}