前言
EVM 是一個輕量級的虛擬機,其設計初衷就是提供一種可以忽略硬件、操作系統等兼容性的虛擬的執行環境供以太坊網絡運行智能合約。
簡單來說EVM 是一個完全獨立的沙盒,在EVM 中運行的代碼是無法訪問網絡、文件系統和其他進程的,以此來避免錯誤的代碼能讓智能合約毀滅或者影響外部環境。
在此基礎上,知道創宇區塊鏈安全實驗室帶大家一起深入理解EVM 的存儲機制和安全問題。
EVM存儲結構
可以看到EVM 存儲數據分為兩類:
-
存儲在code 和storage 裡的數據是non-volatile (不容易丟失的)
-
存儲在stack,args,memory 里數據是volatile(容易丟失的)
各個存儲位置的含義
代碼
code 部署合約時儲存data 字段也就是合約內容的空間,即專門存儲智能合約的二進制源碼的空間
貯存
Storage 是一個可以讀寫修改的持久存儲的空間,也是每個合約持久化存儲數據的地方。 Storage 是一個巨大的map,一共2^256 個插槽(slot),每個插糟有32byte,合約中的“狀態變量”會根據其具體類型分別保存到這些插槽中。
堆
stack 即所謂的“運行棧”,用來保存EVM 指令的輸入和輸出數據。可以免費使用,沒有gas 消耗,用來保存函數的局部變量,數量被限制在16 個。 stack 的最大深度為1024 ,其中每個單元是32 byte。
參數
args 也叫calldata,是一段只讀的可尋址的保存函數調用參數的空間,與棧不同的地方的是,如果要使用calldata 裡面的數據,必須手動指定偏移量和讀取的字節數。
記憶
Memory 一個簡單的字節數組,主要是在運行期間存儲數據,將參數傳遞給內部函數。基於32byte 進行尋址和擴展。
EVM 數據存儲概述
前面已經說過Storage 是每個合約持久化存儲數據的地方其儲存數據的方式是通過插槽來實現的,現在就具體介紹它是怎麼實現的:
狀態變量
1.對於大小在32 字節以內的變量(常量),以其定義的順序作為它的索引值來存儲。即第一個變量的索引為key(0),第二個變量的索引為key(1)…
2.對於連續較小的值,可能被優化存儲在同一個位置,比如:合約中前四個狀態變量都是uint64 類型的,則四個狀態變量的值會被打包成一個32 字節的值存儲在0 位置。
未優化:
pragma solidity ^ 0.4.11;
合同C{
uint256 a = 12;
uint256 c = 12;
uint256 b = 12;
uint256 d = 12;
函數 m() 查看公共返回(uint256,uint256,uint256,uint256){
返回(a,b,c,d);
}
}
優化後:
pragma solidity ^ 0.4.11;
合同C{
uint64 a = 12;
uint64 c = 12;
uint64 b = 12;
uint64 d = 12;
函數 m() 查看公共返回(uint64,uint64,uint64,uint64){
返回(a,b,c,d);
}
}
結構體
對於大小在32 字節以內的結構體同樣也是順序存儲,例如結構體變量索引定義在位置0,結構體內部有兩個成員,則這兩個成員的依序為0 和1。
pragma solidity ^ 0.4.11;
合同C{
結構信息{
uint256 一個;
uint256 b ;
}
函數 m() 外部返回(uint256,uint256){
信息存儲信息;
信息.a = 12;
信息.b = 24;
返回(信息a,信息b);
}
}
映射(map)
map 存儲位置是通過keccak256 (bytes32(key) + bytes32(position) ) 計算得到的,position 表示key 對應storage 類型變量存儲的位置。
pragma solidity ^ 0.4.11;
合約測試{
映射(uint256 => uint256)已知秒;
函數去()公共{
已知安全[0x60] = 0x40;
}
}
數組
定長數組
同上,只要在32 字節以內也是順序存儲,不過在編譯時編譯器會進行邊界檢查防止越界。
pragma solidity ^ 0.4.11;
合同C{
uint256[3] 一 = [12,24,48] ;
函數 m() 公共視圖返回(uint256,uint256,uint256){
返回(一個[0],一種[1],一種[2]);
}
}
可變長度數組
由於可變長度數組長度不定,一般在編譯可變長度數組時會提前預留存儲空間,所以就會使用狀態變量的位置存儲可變長度數組的長度。
而具體的數據地址會通過計算keccak256 (bytes32(position)) 算得數組首地址,再加數組長度偏移量獲得具體的元素。
pragma solidity ^ 0.4.11;
合同C{
uint256[] 一 = [12,24,48] ;
函數 m() 公共視圖返回(uint256,uint256,uint256){
返回(一個[0],一種[1],一種[2]);
}
}
字節數組和字符串
如果長度小於等於31字節:
1.對於定長字節數組則是同定長數組一樣;
2.對於可變字節數組和字符串,會在存儲值位置補0一直到32字節,並用補0的最後一個字節存儲字符串的編碼長度。
pragma solidity ^ 0.4.4;
合約A{
字符串 public name0 = “knownsec”;
bytes8 公共名稱=0x6b6e6f776e736563;
字節公共 g ;
功能測試()公共{
g.push(0xAA);
g.push(0xBB);
g.push(0xCC);
}
函數 go() 公共視圖返回(字節){
返回 g;
}
}
當節數組和字符串長度大於31字節時
1.變量位置存儲編碼長度,並且編碼長度公式更換為編碼長度= 字符數* 2 + 1
2.真實存儲值第一個位置通過公式keccak256(bytes32(position)) 獲取,剩餘值在獲取到的位置順序存儲,同樣在最後存儲位置補0到32字節。
string public name = “knownsecoooooooooooooooooooooooooo”;
安全問題
前面已經講到EVM的存儲結構及存儲機制,現在我們再來探討其安全問題。
未初始化變量
漏洞原理:
在官方手冊中提到結構體,數組和映射的局部變量默認是放在storage 中的,而solidity 語言中函數中設置的局部變量的默認類型取決於它們本身的類型。
因此如果在函數內部設置以上storage 類型變量卻沒有進行初始化,他們就相當於存儲指針指向合約中的其他變量,當我們對其進行改變時改變的就是其指向的變量。漏洞合約,目的修改owner 為自己地址:
pragma 可靠性 ^ 0.4.0;
合約測試合約{
布爾公共解鎖=假;
地址公共所有者 = 0xCA35b7d915458EF540aDe6068dFe2F44E8fa733c;
結構人{
bytes32 名稱;
地址映射地址;
}
功能測試(bytes32 _name,地址_mappedAddress)public{
人人;
person.name = _name;
person.mappedAddress = _mappedAddress;
需要(解鎖);
}
}
漏洞合約分析:
可以看到該合約在函數部分創建新的結構體時沒有進行初始化,由此我們可以利用該函數進行對owner的修改。不過使用該函數我們還要通過require驗證,不過這也不難因為狀態變量unlocked也同樣在我們可控的範圍內。
具體操作:
調用test函數分別傳入向_name 傳入:0x0000000000000000000000000000000000000000000000000000000000000001(真值)
_mappedAddress 傳入:0xfB89eCb0188cb83c220aADDa1468C1635208e821(個人地址)
傳參前:
傳參後:
可以看到已經成功更改了地址。
總結
可以看到EVM 的存儲器就是一個key=>value 的健值數據庫,存儲的數據可以通過校驗和來確保一致。但是其也是和智能合約語言進行交互的,當其中一些規則發生衝突很可能就被別有用心的人用來作惡,所以規範的使用智能合約語言是避開漏洞的必要條件。