By:小白@慢霧安全團隊
背景概述
上次我們了解了solidity 中自帶的函數——自毀函數,相信大家多少已經對它有所了解,這次我們將了解如何訪問合約中的私有數據(private 數據)。
前置知識
我們先來了解一下solidity 中的三種數據存儲方式:
1. storage(存儲)
storage 中的數據被永久存儲。其以鍵值對的形式存儲在slot 插槽中。 storage 中的數據會被寫在區塊鏈中(因此它們會更改狀態),這就是為什麼使用存儲非常昂貴的原因。佔用256 位插槽的gas 成本為20,000 gas。修改storage 的值將花費5,000 gas 。清理存儲插槽時(即將非零字節設置為零),將退還一定量的gas 。 storage 共有2^256 個插槽,每個插槽32 個字節數據按聲明順序依次存儲,數據將會從每個插槽的右邊開始存儲,如果相鄰變量適合單個32 字節,然後它們被打包到同一個插槽中否則將會啟用新的插槽來存儲。
(storage 的存儲方式圖)
storage 中的數組的存儲方式就比較獨特了,首先,solidity 中的數組分為兩種:
a.定長數組(長度固定):
定長數組中的每個元素都會有一個獨立的插槽來存儲。以一個含有三個uint64 元素的定長數組為例,下圖可以清楚的看出其存儲方式:
(定長數組存儲方式圖)
b.變長數組(長度隨元素的數量而改變):
變長數組的存儲方式就很奇特,在遇到變長數組時,會先啟用一個新的插槽slotA 用來存儲數組的長度,其數據存儲在另外的編號為slotV 的插槽中。 slotA 表示變長數組聲明的位置,用length 表示變長數組的長度,用slotV 表示變長數組數據存儲的位置,用value 表示變長數組某個數據的值,用index 表示value 對應的索引下標,則
length = sload(slotA)
slotV = keccak256(slotA) + index
value = sload(slotV)
變長數組在編譯期間無法知道數組的長度,沒辦法提前預留存儲空間,所以Solidity 就用slotA 位置存儲了變長數組的長度。
我們寫一個簡單的例子來驗證上面描述的變長數組的存儲方式:
pragma solidity ^0.8.0;contract haha{ uint[] user; function addUser(uint a) public returns (bytes memory){ user.push(a); return abi.encode(user); }}
部署這個合約後調用addUser 函數並傳入參數a = 998,debug 後可以看出變長數組的存儲方式:
其中第一個插槽為(這裡存儲的是變長數組的長度):
0x290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e563
這個值等於:
sha3('0x0000000000000000000000000000000000000000000000000000000000000000')
key = 0 這是當前插槽的編號
value = 1 這說明變長數組user[] 中只有一條數據也就是數組長度為1 ;
第二個插槽為(這裡存儲的是變長數組中的數據):
0x510e4e770828ddbf7f7b00ab00a9f6adaf81c0dc9cc85f1f8249c256942d61d9
這個值等於:
sha3('0x290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e563')
插槽編號為:
key=0x290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e563
這個值等於:
sha3('0x0000000000000000000000000000000000000000000000000000000000000000')+0
插槽中存儲的數據為:
value=0x00000000000000000000000000000000000000000000000000000000000003e6
也就是16 進製表示的998 ,也就是我們傳入的a 的值。
為了更準確的驗證我們再調用一次addUser 函數並傳入a=999 可以得到下面的結果:
這裡我們可以看到新的插槽為:
0x6c13d8c1c5df666ea9ca2a428504a3776c8ca01021c3a1524ca7d765f600979a
這個值等於:
sha3('0x290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e564')
插槽編號為: key=0x290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e564
這個值等於:
sha3('0x0000000000000000000000000000000000000000000000000000000000000000')+1
插槽中的存儲數據為:
value=0x00000000000000000000000000000000000000000000000000000000000003e7
這個值就是16 進製表示的999 也就是我們剛剛調用addUser 函數傳入的a 的值。
通過上面的例子應該可以大致理解變長數組的存儲方式了。
2. memory(內存)
memory 是一個字節數組,其插槽大小為256 位(32 個字節)。數據僅在函數執行期間存儲,執行完之後,將會被刪除。它們不會保存到區塊鏈中。讀或寫一個字節(256 位)需要3 gas 。為了避免給礦工帶來太多工作,在進行22 次讀寫操作後,之後的讀寫成本開始上升。
3.calldata(調用數據)
calldata 是一個不可修改的,非持久性的區域,用於存儲函數參數,並且其行為基本上類似於memory。調用外部函數的參數需要calldata,也可用於其他變量。它避免了複製,並確保了數據不能被修改。帶有calldata 數據位置的數組和結構體也可以從函數中返回,但是不可以為這種類型賦值。
了解了solidity 中的三種存儲方式後我們再來了解一下合約中的四種可見性關鍵字:在solidity 中,有四種可見性關鍵字:external,public,internal 和private。默認時函數可見性為public。對狀態變量而言,除了不能用external 來定義,其它三個都可以來定義變量,狀態變量默認的可見性為internal。
1.external 關鍵字
external 定義的外部函數可以被其它合約調用。用external 修飾的外部函數function() 不能作為內部函數直接調用,也就是說function() 的調用方式必須用this.function() 。
2.public 關鍵字
public 定義的函數可以被內部函數或外部消息調用。對用public 定義的狀態變量,系統會自動生成一個getter 函數。
3.internal 用關鍵字
internal 定義的函數和狀態變量只能在(當前合約或當前合約派生的合約)內部進行訪問。
4.private 關鍵字
private 定義的函數和狀態變量只對定義它的合約可見,該合約派生的合約都不能調用和訪問該函數及狀態變量。
綜上可知,合約中修飾變量存儲的關鍵字僅僅限制了其調用的範圍,並沒有限制其是否可讀。所以我們今天就來帶大家了解如何讀取合約中的所有數據。
漏洞示例
這次我們的目標合約是部署在Ropsten 上的一個合約。
合約地址:
0x3505a02BCDFbb225988161a95528bfDb279faD6b
鏈接:
https://ropsten.etherscan.io/address/0x3505a02BCDFbb225988161a95528bfDb279faD6b#code
這裡我也給大家把合約源碼展示出來:
contract Vault { uint public count = 123; address public owner = msg.sender; bool public isTrue = true; uint16 public u16 = 31; bytes32 private password; uint public constant someConst = 123; bytes32[3] public data; struct User { uint id; bytes32 password; } User[] private users; mapping(uint => User) private idToUser; constructor(bytes32 _password) { password = _password; } function addUser(bytes32 _password) public { User memory user = User({id : users.length, password: _password}); users.push(user); idToUser[user.id] = user; } function getArrayLocation( uint slot, uint index, uint elementSize) public pure returns (uint) { return uint( keccak256(abi.encodePacked(slot))) + (index * elementSize); } function getMapLocation(uint slot, uint key) public pure returns (uint) { return uint(keccak256(abi.encodePacked(key, slot))); }}
漏洞分析
由上面的合約代碼我們可以看到,Vault 合約將用戶的用戶名和密碼這樣的敏感數據記錄在了合約中,由前置知識中我們可以了解到,合約中修飾變量的關鍵字僅限制其調用範圍,這也就間接證明了合約中的數據均是公開的,可任意讀取的,將敏感數據記錄在合約中是不安全的。
讀取數據
下面我們就帶大家來讀取這個合約中的數據。首先我們先看slot0 中的數據:
由合約中可以看到slot0 中只存儲了一個uint 類型的數據,我們讀取出來看一下:
我這裡使用Web3.py 取得數據
首先寫好程序
運行後得到
我們使用進制轉換器轉換一下
這裡我們就成功的去到了合約中的第一個插槽slot0 中存儲的uint 類型的變量count=123 ,下面我們繼續:
slot1 中存儲三個變量:u16, isTrue, owner
從右往左依次為
owner = f36467c4e023c355026066b8dc51456e7b791d99
isTrue = 01 = true
u16 = 1f = 31
slot2 中就存儲著私有變量password 我們讀取看看
slot 3, 4, 5 中存儲著定長數組中的三個元素
slot6 中存儲著變長數組的長度
我們從合約代碼中可以看到用戶的id 和password 是由鍵值對的形式存儲的,下面我們來讀取兩個用戶的id 和password:
user1
user2
好了,這裡我們就成功的將合約中的所有數據讀取完成,現在大家應該都能得出一個結論:合約中的私有數據也是可以讀取的。
修復建議
(1)作為開發者
不要將任何敏感數據存放在合約中,因為合約中的任何數據都可被讀取。
(2)作為審計者
在審計過程中應當注意合約中是否存在敏感數據,例如:秘鑰,遊戲通關口令等。
參考文獻
本期講解的知識有點偏底層,可以參考以下文章幫助你更好地理解:
https://solidity-by-example.org/hacks/accessing-private-data/
《跟我學Solidity : 變量的存儲》https://learnblockchain.cn/article/1759《快速入門——web3.py》https://web3py.readthedocs.io/《狀態變量、函數的權限》https: //blog.csdn.net/liyuechun520/article/details/78408608