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