從MEV大佬的程式碼工程中,我學到了什麼?
程式碼的複雜性與藝術表現相結合,形成了我所見過最複雜的程式碼混淆之一。
原文標題:《To Reverse A Big Brain》
原文作者:@jtriley_eth,twitter
原文編譯:zhouzhou,BlockBeats
編按:在逆向bigbrainchad.eth 的MEV 合約過程中,我們發現其使用了複雜的程式碼混淆和堆疊打亂技術,使得自動化工具難以處理。合約中有大量不必要的程式碼區塊和循環,並透過動態跳轉和時間戳控制進一步增加了複雜性。儘管使用了 Heimdall 和 Tenderly 等工具,進展有限,最終我們轉向 Python 進行手動分析,嘗試透過逐塊解析和堆疊註釋尋找突破口,這次逆向工程充滿挑戰但仍未完全解讀。
Bigbrainchad.eth 是一個執行MEV 合約的帳戶,其中每筆交易雜湊都以0xbeef 開頭,每個函數呼叫以codeIsLaw 開頭,每個交易輸入以0xdeadbeef 開頭。高層來看,它是一個多呼叫者(multicaller)。但在底層,程式碼的複雜性與藝術表現相結合,形成了我所見過最複雜的程式碼混淆之一。
初步攻擊
逆向工程智能合約,我們先從自動化工具開始。每個逆向工程師都浪費了大量時間在那些工具本來可以自動完成的任務上。因此我們將從一些流行工具入手。
Heimdall
Heimdall 是北歐神話中彩虹橋的守護者,而在以太坊的世界中,它是一個字節碼反編譯工具。 Heimdall 提供了多個反編譯層次,所以我們從最高層次的 Solidity 開始。
要注意的是,Heimdall 輸出的並不是一般可以編譯的「有效」Solidity,它更像是一個啟發式工具,幫助我們更好地理解高層控制流。
heimdall decompile bytecode.hex --include-sol
這麼簡單嗎?沒那麼容易,字串中混入了一些空字節,這可能是 bigbrainchad 的疏忽,寫了太多彙編程式碼。我們再深入一層,看看 Yul。
heimdall decompile bytecode.hex --include-yul
你有沒有過這種感覺:原本覺得是個有趣的小項目,突然陷入失控狀態,深入到EVM(以太坊虛擬機)的深淵中,你計畫好的一整天突然消失,像是「Mr. Stark,我感覺不太好」?
Solidity 和 Yul 的輸出完全不同。某些條件相符,例如 calldatasize 非零或交易發起者和呼叫者匹配,但這些條件在 Yul 中並不會導致回滾(revert)。我們再深入一點,進入所謂的控制流程圖(Control Flow Graph)。
heimdall cfg bytecode.hex
控制流程圖將智慧合約的字節碼分成多個區塊,依據程式碼選擇不同路徑的地方進行分割;這可能是在你需要基於某種錯誤有條件地回溯交易時,也可能是類似Uniswap V3 中tick math 的某個閾值條件。上面展示的完整內容是為了增強戲劇效果,但其中隱藏了一個微妙的問題。
jumpi 指令是條件跳轉指令,它告訴EVM(以太坊虛擬機)「如果」某個條件是非零,則「跳轉」到另一處程式碼繼續執行,否則就像什麼都沒發生一樣繼續執行下去。通常,這會顯示為一個區塊指向兩個其他區塊,一個代表“為真”條件,另一個代表“為假”條件,類似於以下情況。
如果條件為「真」,則回溯交易,否則成功回傳。這在 Solidity 的 require 語句中非常常見。
這非常好,可以幫助我們繪製控制流程圖,但這裡有一個小問題。如果我們向上查看控制流程圖的上一個區塊,會看到如下情況。
字節碼中不可能存在只有一個結果的條件跳躍。
我們看到一個區塊裡只有條件跳轉後的單一結果。雖然這在高階語言中是技術上可行的,但在字節碼中不應該出現這種情況。每個 jumpi 指令都應該有兩個結果,即便其中一個是完全無效的,它也應該在程式碼中體現出來。那麼,這究竟是怎麼回事?
我們現在深入討論。一般來說,高階語言如 Solidity 或 Vyper 會自己處理所有控制流程邏輯,這意味著無法直接使用 jump 或 jumpi 指令。它們只能使用像 if、switch 或函數來呼叫這樣的結構,即便是在 Solidity 的「內聯彙編」(也稱為中間表示,Yul)中也是如此。
這意味著所有的jump 和jumpi 指令在編譯時都是已知的,它們無法來自於像calldata 這樣不可預測的資料(而這正是我們這裡看到的情況)。這暗示著這個合約要么是用彙編語言寫的,要么是使用了 Yul 優化器引入前的舊版本 Solidity 編寫的。嚴格來說,如果沒有使用Yul 管道,你可以透過calldata 覆蓋函數指標以達到這種效果,但在使用「via-ir」(透過中間表示)的情況下,即使是函數指標也不再是直接的跳轉目標,而是透過函數ID 進行分發。
無論是哪種情況,Heimdall 都無法進一步幫助我們,我們需要繼續探索其他工具。
Tenderly
Tenderly 提供了一些交易追蹤訊息,並且在過去的漏洞恢復和回應中起到了關鍵作用。讓我們看看是否能透過它對這個程式的運作方式獲得有意義的洞察。我們將使用以下交易雜湊:0xbeef0ad930c2f0052758ce9ce579ad7f83bed913ffedb50046c3c674293d1fe5
Tenderly dashboard
沒有原始碼,所以無法進行偵錯.不過,它確實包括了從高層次的呼叫資訊。雖然這些稍後可能會有用,但現在我們需要實際的控制流步驟來查看底層到底發生了什麼。
Bytegraph
在此之前我沒有聽說過這個應用程序,但它也提供控制流程圖,看起來更加有趣,甚至可以透過拖放來操作。讓我們將合約的字節碼輸入進去,看看會有什麼發現。
Bytegraph
說實話,這裡有其他區塊,也許是我還不太會使用這個工具,但我可不打算手動找到並連接這些節點。我們繼續吧。
EVM Codes
我之前對EVM dot codes 給予了很多好評(現在依然如此)。它包含了每個 EVM 操作碼的資訊(如果你想深入研究,建議每一個都讀一遍,每一個),並且還有一個可以測試操作碼的 playground。讓我們試試將字節碼和 calldata 插進去,逐步執行我們之前提到的交易。
evm.codes
我們在重新執行之前的交易時遇到了運行時回滾錯誤。雖然這裡沒有顯示,但錯誤訊息只是一個單字:「time」(時間)。事實證明,這個檢查會將區塊的時間戳記減少到 16 位,然後從中減去 calldata 的第 5 和第 6 位元組。就我所知,EVM Codes 沒有環境變數覆蓋功能,所以這次嘗試沒成功。
Python3 和一個夢想
到目前為止,進展非常有限,所以讓我們試試 Python。我們將逐塊分解程式碼,按 jumpdest、jumpi、jump、stop、revert 和 invalid 進行劃分。現在我們先把這些內容寫入文件,然後開始加入一些堆疊註釋,希望能找到突破口。
Comments indicating stack state
這個過程非常痛苦,但有幾件事變得顯而易見。
首先,堆疊調度非常混亂,這可能只能歸因於舊版的Solidity 或我們第一次遇到的程式碼混淆-堆疊打亂。
堆疊打亂是透過改變變數在堆疊中的添加、移動和使用順序,以故意誤導逆向工程師的一種手段。例如,考慮以下兩個程式碼片段,它們的功能完全相同。
第一個程式碼片段很直接,我們將輸入推送給mstore,它將一個值儲存在記憶體中,然後將輸入推送給return,它將記憶體的一部分傳回給呼叫者。第二個程式碼片段做了完全相同的事情,但方式不那麼直接。
不幸的是,Bigbrainchad 的堆疊打亂技巧要複雜得多。
還有一種程式碼混淆技術,它將常數分解為其他值,並在運行時進行算術運算來產生實際的常數。將這種技術與堆疊打亂結合起來,你會發現堆疊中堆積了30 個看似無用的值,但隨著程式的運行,這些值會逐一被重新組合成程式實際需要的值,簡直讓人抓狂。
傳奇工具登場:The GOAT
在這過程中,幾個小時後我收到一則訊息,@plotchy 開發了一款工具,它透過符號執行來建立控制流程圖,不僅做了其他控制流程圖工具的工作,還能夠映射出那些透過calldata 等資料衍生出的間接跳躍。經過一些依賴項的調整,我們得到如下圖的結果。
evm-cfg bytecode.hex -o plotchy /cfg.dot --open
太棒了!雖然完整圖像是為了增加戲劇效果,但它確實包含了一些不錯的高亮顯示,幫助我們找到失敗和成功的程式碼路徑,並明確標記哪個分支是真(truthy),哪個分支是假(falsy)。不過,這並不總是顯而易見的。
然而,這也帶來了我們遇到的第二和第三種程式碼混淆技術。
首先,有大量不必要的程式碼區塊。有些跳轉只發生一次,只有一個程式碼區塊被引用。也許這是舊版 Solidity 的問題,就像微軟那樣注入了大量膨脹軟體,或者這裡存在一些程式碼混淆。
其次,有大量循環。有些迴圈只在特定的 calldata 位元組具有特定值時發生,有些則是上面提到的那些動態跳轉的結果,似乎有多達十個這樣的動態跳躍。有些循環很短,而有些幾乎貫穿整個合約。也許我有些陰謀論,但其中一些可能只是“蜜罐”(honey pots)。
EVM-CFG 迴圈
第二次攻擊
到目前為止,除了發現一些非常精妙的程式碼混淆技巧和可能的「心理戰」外,還沒有太大的成功。因此,讓我們嘗試另一個角度。我們將回到前面提到的 Tenderly 部分的交易,並嘗試從高層次的追蹤資訊中推測其發生了什麼。
該交易的calldata 如下圖所示:
在這筆交易中,MEV 合約只執行了兩個操作。
第一個操作是呼叫了一個代幣的 distributeETH 函數,且傳遞的值為零。由於合約是通用的,因此不可能儲存每個合約位址、選擇器和呼叫值,必須透過 calldata 注入。
第二個操作是呼叫了原始呼叫者 bigbrainchad.eth。這次沒有 calldata,但有呼叫值。
考慮到這些,我們可以將 calldata 進行如下分解。
前四個位元組是裝飾性的,合約會忽略它們。先前對該合約的調用實際上使用的是全零 0x00000000,因此在效率最大化的過程中逐漸加入了一些風格化的元素。接下來的兩個位元組對應的是前面提到的時間戳控制流;這很可能是為了確保交易只會在預期的區塊和預期的時間戳記內被包含。再接下來的一個位元組與我之前提到的控制流蜜罐有關;也許它有更重要的用途,但堆疊太深,我也有些累了。隨後還有一些未知位元組。
真正有趣的是「目標位址大小」參數。對於呼叫的目標位址、值和負載,每個都有一個前綴大小。它們使用一個位元組表示目標位址和值的大小,並使用兩個位元組表示負載的大小。系列中的第一個呼叫是針對 0xf193..65b1,值為零,負載為 0xb8b9b54900,即 distributeETH()。系列中的第二個呼叫是針對 0xd215..0dfd,即 bigbrainchad.eth,值為 281314951393027 wei,且沒有 calldata。
總結
儘管我非常想完成這項工作,發布一些源代碼,並製作一個模仿合約,但這個合約讓我折服了。要解開的內容太多了,手動追蹤的程式碼路徑也太多,自動化工具面對的歧義也太多。如果你想查看我工作的筆記,可以透過下面的 GitHub 連結查看,但現在,我暫且放棄。
如果 bigbrainchad.eth 看到了這篇文章,祝你好運,你的合約真是一件藝術品。
免責聲明:文章中的所有內容僅代表作者的觀點,與本平台無關。用戶不應以本文作為投資決策的參考。
您也可能喜歡
川普食言「不支持加密」就慘了!富比世警告:投資者別高興太早
富爸爸公開比特幣持倉「73枚」,計畫加倉至100枚:上漲還沒封頂!
MANTRA 開放資格檢查並公佈主網 Gendrop 分配
簡單來說 MANTRA 已開放 OM 代幣 airdrop 檢查員並透露了主網GenDrop的詳細信息,獎勵分配給350,000名符合條件的用戶。