深入EVM-合約分類這件小事背後的風險

在智能合約領域,"以太坊虛擬機EVM" 以及其算法和數據結構就是第一性原理。

本文從合約為什麼要分類出發,結合每個場景可能面對怎樣的惡意攻擊,最終給出一套達成相對安全的合約分類分析算法。

**雖然技術含量較高,但亦可作為雜談讀物,**一覽去中心化系統間博弈的黑暗森林。

1、合約為什麼要分類?

因為太重要了,可謂是交易所、錢包、區塊鏈瀏覽器、數據分析平台等等Dapp的基石!

一筆交易之所以是ERC20轉賬,是因為他的行為符合ERC20標準,至少得有:

  1. 交易的狀態是成功
  2. To地址為某個符合ERC20標準的合約
  3. 調用了Transfer函數,其特點是該交易CallData的前4位為0xa9059cbb
  4. 執行後,在該To地址上發出了transfer的事件

分類有誤則交易行為會誤判

以交易行為為基石,則To地址能否被準確分類則對其CallData的判斷會有截然不然的結論。對Dapp而言,鏈上鍊下的信息溝通高度依賴於交易事件的監聽,而同樣的事件編碼也只有在符合標準的合約中發出,才具有可信度。

分類有誤則交易會誤入黑洞

如果用戶進行一筆Token轉移,轉入到某個合約中,如果該合約沒有預設Token轉出的函數方法,則資金會雷同於Burn一樣被鎖定,無法控制

且如今大量項目開始增加內置的錢包支持,要為用戶管理錢包也就不可避免的,需要時刻從鏈上實時分類出最新部署的合約,是否能夠吻合資產標準。

深入EVM-合約分類這件小事背後的風險

2、分類會有怎樣的風險?

**鏈上是一個沒有身份沒有法治的地方,你無法制止一筆正常的交易,哪怕他是惡意的。 **

他可以是冒充外婆的狼,做出多數符合你預期的外婆行為,但目的是進屋搶劫。

聲明標準,但可能實質不符合

常見的分類方式是直接採用EIP-165標準,讀取該地址是否支持ERC20等,當然,這是一個高效率的方法,但是畢竟合約是對方控制,所以終究是可以偽造出一份申明。

165標準的查詢,只是在鏈上有限的操作碼中,用最低成本去防止資金轉入黑洞的方法。

這也是為什麼我們之前分析NFT的時候,特地提及標準中會有一類SafeTransferFrom的方法,其中Safe就是指代了採用165標準判斷出對方聲明自己具備了NFT的轉移能力。

唯有從合約字節碼出發,做源碼層面的靜態分析,從合約預期的行為出發才有更精準的可能性。

3、合約分類方案設計

接下來咱們將系統的分析整體方案,注意我們的**目的是“精度”和“效率”兩項核心指標。 **

要知道即使方向是對的,但要抵達大洋的彼岸路途也並不明朗,要做字節碼分析的第一站是獲取代碼

3.1、如何獲取到代碼?

從上鍊後的角度講有getCode,一個RPC方法,可以從鏈上指定的地址裡獲取到字節碼,單論讀取的話這是非常快捷的,因為從EVM的賬號結構中就把codeHash放在最頂端的位置。

深入EVM-合約分類這件小事背後的風險

但是這個方法等於是單獨對某個地址做獲取,想要進一步提升精度和效率呢?

如果是部署合約的交易,如何在其剛執行完甚至他還在內存池中便獲取部署的代碼?

如果該筆交易是合約工廠的模式,則交易的Calldata裡是否存在源碼呢?

最後的我的方式是,是分類進行一種類似篩子的模式

  1. 對於非合約部署的交易,則直接用getCode獲取其中涉及的地址進行分類,
  2. 對於最新內存池的交易,篩選出to地址為空的交易,其CallData則是帶有構造函數的源代碼
  3. 對於合約工廠模式的交易,由於其中可能是合約部署出的合約再循環調用其他合約來執行部署,則遞歸的去分析該筆交易的子交易,記錄每個type 為CREATE或者為CREATE2的Call。

我做了個demo實現的時候,發現還好現在rpc的版本比較高,因為整個過程最難的便是執行3的時候,如何遞歸找到指定type的call,最底層的方式是通過opcode還原上下文,我吃了一驚!

還好現在的geth版本里有debug_traceTransaction 方法,他可以幫助解決在通過opcode操作碼中梳理每一個call的上下文信息,整理出核心的字段。

最終可以對多種部署模式的(直接部署,工廠模式單部署,工廠模式批量部署)的原始字節碼都獲取到。

3.2、如何從代碼分類?

最最簡單但不安全的方式,是把code直接做字符串匹配,以ERC20為例符合標準的函數則有

深入EVM-合約分類這件小事背後的風險

在函數名之後的,則是該函數的函數簽名,之前在分析的時候提及,交易都是依賴匹配callData的前4位找到目標函數的,拓展閱讀:

所以合約字節碼裡必然存儲有這6個函數的簽名。

當然,這種方法非常快捷6個都查到就完事的,但不安全的因素則是,如果我採用solidity合約中,單獨設計一個變量,存儲值為0x18160ddd 那麼他也會將認為我有了這個函數。

3.3、準確率提升1-反編譯

那進一步的準確方法則是做Opcode的反編譯!反編譯則是將獲取到的字節碼轉到操作碼的過程,更高級的反編譯則是再轉成偽代碼,更利於人的閱讀,這次我們用不上,反編譯的方法列於文末的附錄中。

solidity(高級語言)->bytecode(字節碼)->opcode(操作碼)

我們就可以清晰的發現一個特徵,函數簽名都會被PUSH4 這個操作碼所執行,所以進一步的方法則是從全文中提取PUSH4後的內容,與函數標準做匹配。

深入EVM-合約分類這件小事背後的風險

我也簡單做了下性能實驗,不得不說Go語言的效率很強大,1W次反編譯只需要220ms。

接下來的內容會有一定難度

3.4、準確率提升2-找代碼塊

上文中準確率有所提升但還不夠,因為是全文搜索PUSH4的,因為我們仍然可以構建一個變量,是byte4的類型,這樣一來也會觸發PUSH4的指令。

在我苦惱的時候,想到一些開源項目的實現,ETL是一個讀取鏈上數據做分析的工具,其中會解析出ERC20、721的轉移單獨成表,所以必然具備分類合約的能力。

深入EVM-合約分類這件小事背後的風險

分析下來,可以發現他是基於代碼塊的分類,只處理第一個basic_blocks [0] 裡的push4指令

那問題來到了,如何準確判斷代碼塊了

代碼塊的概念源於REVERT + JUMPDEST 這2個連續的操作碼,這裡必然需要連續的2個,因為在整個函數選取器的opcode區間裡,如果函數數量過多,則會出現翻頁的邏輯,那也會出現JUMPDEST 這個指令。

深入EVM-合約分類這件小事背後的風險

3.5、準確率提升3-找函數選擇器

函數選擇器的作用是,讀取該筆交易的Calldata的前4位字節,並與代碼中預設有的合約函數簽名進行匹配,協助指令跳轉到存儲了該函數方法指定的內存位置

讓我們嘗試一個最小的模擬執行

這部分是兩個函數的選擇器store(uint 256)和retrieve(),可算出簽名是2e64cec1,6057361d

深入EVM-合約分類這件小事背後的風險

進行反編譯後,則會得到如下的操作碼串,可以說分兩個部分

第一部分:

在編譯器中在合約中僅函數選擇器部分會去獲取到callData的內容,寓意是獲取其CallData的函數調用簽名,註釋如下圖。

深入EVM-合約分類這件小事背後的風險

我們可以通過模擬EVM的內存池變化來看看效果

深入EVM-合約分類這件小事背後的風險

第二部分:

判斷是否與選擇器的值匹配的過程

1、將retrieve()的4字節函數簽名(0x2e64cec1)傳入stack上,

2、EQ操作碼從stack區彈出2個變量,即0x2e64cec1和0x6057361d,並檢查它們是否相等

3、PUSH2將2個字節的數據(這里為0x003b,十進制為59)傳入stack,stack區有一個叫做程序計數器的東西,它規定了下一個執行命令在字節碼中的位置。這裡我們設置59,因為那是retrieve()字節碼的起始位置

4、JUMPI代表"如果...,則跳轉至...",它從stack中彈出2個值作為輸入,如果條件為真,程序計數器將被更新至59。

這就是EVM是如何根據合約中的函數調用,來確定它需要執行的函數字節碼的位置的原理。

實際上,這只是一組簡單的“if語句”,用於合約中的每個函數以及它們的跳轉位置。

深入EVM-合約分類這件小事背後的風險

4、方案總結

整體簡述如下

  1. 每個合約地址可以通過rpcgetcode 或者debug_traceTransaction,獲取到部署後的bytecode ,採用GO中VM和ASM庫,反編譯後即獲取到opcode
  2. 合約在EVM運行原理中,會有以下特徵
  • 採用REVERT+JUMPDEST這2個連續的opcode 作為代碼塊的區分
  • 合約必然具備函數選擇器的功能,該功能也必然在第一個代碼塊上
  • 函數選擇器中,其函數方法均採用PUSH4作為opcode ,
  • 該選擇器所包含的opcode中,會出現連續的PUSH1 00; CALLDATALOAD; PUSH1 e0; SHR; DUP1,核心功能是加載callDate數據並進行位移操作,從合約功能上其他語法不會產生
  1. 對應的函數簽名在eip中定義,並且有必选和可選的明確說明

4.1、唯一性證明

走到這裡我們就可以說,基本實現高效率,高準確率的合約分析方法了,當然既然已經嚴謹了這麼久,不妨再嚴謹一些,我們上文方案里中基於REVER+JUMPDEST來做代碼塊的區分,結合其中必然的CallDate加載和位移來做唯一性判斷,那是否存在,我可以用solidity合約也實現出類似的操作碼序列呢?

我做了下對照實驗,從solidity語法層面雖然亦有msg.sig等獲取CallData的方法,但編譯後其opcode的實現方法不同

深入EVM-合約分類這件小事背後的風險

查看原文
本頁面內容僅供參考,非招攬或要約,也不提供投資、稅務或法律諮詢。詳見聲明了解更多風險披露。
  • 讚賞
  • 留言
  • 分享
留言
0/400
暫無留言
交易,隨時隨地
qrCode
掃碼下載 Gate.io APP
社群列表
繁體中文
  • 简体中文
  • English
  • Tiếng Việt
  • 繁體中文
  • Español
  • Русский
  • Français (Afrique)
  • Português (Portugal)
  • Indonesia
  • 日本語
  • بالعربية
  • Українська
  • Português (Brasil)