深入理解 EVM 存儲機制及安全問題

前言

EVM 是一個輕量級的虛擬機,其設計初衷就是提供一種可以忽略硬件、操作系統等兼容性的虛擬的執行環境供以太坊網絡運行智能合約

簡單來說 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。

深入理解 EVM 存儲機制及安全問題

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);
   }
}

深入理解 EVM 存儲機制及安全問題

優化后:

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);
   }
}

深入理解 EVM 存儲機制及安全問題

結構體

對於大小在 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);
   }
}

深入理解 EVM 存儲機制及安全問題

映射(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;
 }
}

深入理解 EVM 存儲機制及安全問題

數組

定長數組

同上,只要在 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]);
   }

}

深入理解 EVM 存儲機制及安全問題

可變長度數組

由於可變長度數組長度不定,一般在編譯可變長度數組時會提前預留存儲空間,所以就會使用狀態變量的位置存儲可變長度數組的長度。

而具體的數據地址會通過計算 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]);
   }

}

深入理解 EVM 存儲機制及安全問題

字節數組和字符串

如果長度小於等於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;
   }
}

深入理解 EVM 存儲機制及安全問題

當節數組和字符串長度大於31字節時

1.變量位置存儲編碼長度,並且編碼長度公式更換為編碼長度 = 字符數 * 2 + 1

2.真實存儲值第一個位置通過公式 keccak256(bytes32(position)) 獲取,剩餘值在獲取到的位置順序存儲,同樣在最後存儲位置補0到32字節。

string public name = "knownsecooooooooooooooooooooooooo";

深入理解 EVM 存儲機制及安全問題

安全問題

前面已經講到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 存儲機制及安全問題

傳參后:

深入理解 EVM 存儲機制及安全問題

可以看到已經成功更改了地址。

總結

可以看到 EVM 的存儲器就是一個 key=>value 的健值數據庫,存儲的數據可以通過校驗和來確保一致。但是其也是和智能合約語言進行交互的,當其中一些規則發生衝突很可能就被別有用心的人用來作惡,所以規範的使用智能合約語言是避開漏洞的必要條件。

本文鏈接:https://www.8btc.com/article/6693684

轉載請註明文章出處

(0)
上一篇 2021-10-04 09:08
下一篇 2021-10-04 10:08

相关推荐