【Ethernaut x Foundry】Level 12-Privacy 答案解說

tanner.dev
6 min readMar 16, 2024

--

Figure 1: https://ethernaut.openzeppelin.com/level/0x131c3249e115491E83De375171767Af07906eA36

你以為你擁有隱私權,真的只是你以為

看完這篇你會學到:

  1. Private Variable 特性
  2. Storage Layout 排序規則

題目

此次目標是解鎖合約,把 locked 狀態變成 False 即可通關。

Figure 2: Privacy 題目

合約程式碼如下:

contract Privacy {

bool public locked = true;
uint256 public ID = block.timestamp;
uint8 private flattening = 10;
uint8 private denomination = 255;
uint16 private awkwardness = uint16(block.timestamp);
bytes32[3] private data;

constructor(bytes32[3] memory _data) {
data = _data;
}

function unlock(bytes16 _key) public {
require(_key == bytes16(data[2]));
locked = false;
}

/*
A bunch of super advanced solidity algorithms...

,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`
.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,
*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^ ,---/V\
`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*. ~|__(o.o)
^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*'^`*.,*' UU UU
*/
}

解說

這題和 Level-08 Vault 的考點非常類似,同樣涉及如何讀取 private variable 和 storage layout 的儲存變數規則。

此次目標是解鎖合約,要透過呼叫 unlock() 去把狀態改成解鎖,不過這個函式需要正確的鑰匙 bytes16 _key 且符合 data[2] 數據才能解鎖,因此首要任務是想辦法找出這把鑰匙。

還記得在第八關時,提到了 private variable 和 storage slot 的觀念,智能合約上的變數設定為 private,僅表示它只供內部合約存取,無法從外部或繼承合約存取,但還是可以查看到 private 變數的數值!而在 Foundry 查找變數的 cheatcode 是 vm.load(),要給兩個參數,contract address 和 slot 位置。contract address 我們已知,接下來就是要釐清 slot 位置,但要怎麼知道 data[2] 它所在的 slot 在哪?

可以自己動手數!由於合約的 storage variable 已知,剩下只要按照順序計算每個變數大小,按規則寫入 slot 答案便出來了。以下先複習常見變數的 bytes 大小:

  • bool:1 byte
  • address:20 bytes
  • uint256 / int256:32 bytes
  • uint8 / int8:1 byte (後綴數字除以 8 依此類推)
  • 固定大小的 bytes1 / bytes2 / … / bytes32:大小等同後綴數字
  • 固定陣列 bytes32[3]:每個元素皆 bytes32 (取決於陣列的資料型態)

因為這題沒有使用到動態陣列或 string,所以可以先 pass 他們。知道合約每個變數大小之後,再來是照順序安排進 storage slot,如同之前文章所說:slot 是從 0 開始,每個 slot 可以存 32 bytes 長度,同一個 slot 裡的變數大小加總超過 32 bytes 則會往下一個 slot 儲存。照著這個規則整理,可以得到如下圖的 storage layout:

Figure 3: Storage Slot Layout

而我們要找的 data[2] 儲存在 slot 5!最後,記得用拿到的 key 當作參數去 call unlock(bytes16) 就大功告成啦!

BTW,固定長度的 value type variable 在排 layout 的時候很直覺,slot 能塞就塞,不能塞就往下一個塞,但如果遇到 dynamically-sized variable (dynamic-array / string) 或 mapping 的排法會稍微不同,可以參考這篇文章,解釋得很讚!

完整 solution script 如下,這題也同樣不需要攻擊合約即可通關~

// script/solutions/12-Privacy.s.sol

contract PrivacySolution is Script, EthernautHelper {
...

function run() public {
...

// YOUR SOLUTION HERE

/**
* Understanding Solidity’s Storage Layout And How To Access State Variables In Storage Slots.
*/
bytes32 key = vm.load(challengeInstance, bytes32(uint256(5)));
challengeInstance.call(abi.encodeWithSignature("unlock(bytes16)", bytes16(key)));

...
}
}

參考連結

--

--

tanner.dev

Blockchain Dev | Smart Contract Security | Day 1 in CTF | HODL & BUIDL & BEHODL