0x0 前言
之前, 我們發現了一個Aptos Moveevm 的嚴重漏洞,經過深入研究,我們發現了另外一個新的整數溢出漏洞,這次一的漏洞觸發過程相對更有趣一點, 下面是對這個漏洞的深入分析過程,裡面包含了很多Move語言本身的背景知識.通過本文講解相信你會對move語言有更深入的理解。
眾所周知,Move 語言在執行字節碼之前會驗證代碼單元。驗證代碼單元的過程,分為4步。這個漏洞就出現在reference_safety 的步驟中。
如上面代碼所示, 此模塊定義了用於驗證過程主體的引用安全性的轉移函數。其檢查包括(但不限於)驗證沒有懸空引用、對可變引用的訪問是否安全、對全局存儲引用的訪問是否安全。
下面是引用安全驗證入口函數, 它將調用analyze_function.
在analyze_function中,函數將對每一個基本塊進行驗證,那麼什麼是基本塊呢?
在代碼編譯領域,基本塊是一個代碼序列,除了入口之外沒有分支指令,除了出口之外沒有分支指令。
Move語言是如何識別基本塊?
在Move 語言中, 基本塊是通過遍歷字節碼、查找所有分支指令以及循環指令序列來確定的。以下是核心代碼:
接下來,我們來分享一個move ir代碼基本塊的例子, 如下所示, 它有3 個基本塊。分別由分支指令:BrTrue,Branch,Ret確定。
0x1 Move中的引用安全
參考Rust語言的思想,Move 支持兩種類型的引用類型。不可變引用&(例如&T)和可變引用&mut(例如&mut T)。你可以使用不可變(&) 引用從結構中讀取數據,使用可變(&mut)引用修改它們。通過使用恰當的引用類型,有助於維護安全性以及識別讀取模塊。這樣可以讓讀者清晰地知道此方法是更改值還是僅讀取。下面是官方Move教程中的示例:
在示例中,我們可以看到mut_ref_t是t 的可變引用。
所以在Move 引用安全模塊中,嘗試通過以函數為單元,掃描函數中的基本塊中的字節碼指令驗證判斷所有引用操作是否合法。
下圖顯示了驗證引用安全性的主要流程。
這裡的state是AbstractState結構體, 它包含了borrow graph 和locals ,他們共同用於確保引用函數中的引用安全性。
這裡borrow graph是用來表示局部變量引用之間關係的圖。
從上圖中可以看到, 這裡有一個pre state ,其包含locals 和borrow graph (L ,BG)。然後執行了basic block 生成一個post state (L', BG')。然後將前後的state合併以更新塊狀態並將該塊的後置條件傳播到後續塊。這就像V8 turbofan中的Sea of Nodes思想。
下面的代碼是上圖對應的主循環。首先, 執行塊代碼(如果執行指令不成功,將返回AnalysisError)然後嘗試通過join_result是否更改,來合併pre state和post state。如果更改並且當前塊本身包含一個後向的邊指向自己(這意味著有一個循環)將跳回到循環的開頭, 在下一輪循環仍將執行此基本塊,直到post state等於pre state或因某些錯誤而中止。
因此在引用安全模塊,如何判斷join的結果是否改變?
通過上面的代碼,我們可以通過判斷locals和borrow關係是否發生變化來判斷join結果是否發生變化。這裡的join_ 函數用於更新本地變量和borrow關係圖。
下面是join_ 函數代碼,第6 行是初始化一個新的locals Map 對象。第9 行迭代locals 中的所有索引,如果pre state與post state都值為None,則不要插入到新的locals 映射中,如果pre state 有值,post state為None,則需要釋放brow_graph id ,意味著這裡消除該值的借用關係, 反之亦然。特別的,當pre state與post state 兩個值都存在且相同時,像第30-33行一樣將它們插入到新的map中,然後在第38行合併borrow graph。
通過上面代碼,我們可以看到self.iter_locals() 是locals變量的個數。請注意,此局部變量不僅包括函數的真實局部變量,還包括參數。
0x2 漏洞
在這裡我們已經覆蓋了所有與漏洞相關的代碼,你找到漏洞了嗎? ?
如果你沒有發現漏洞也沒關係,下面我會詳細說明漏洞觸發過程。
首先在下面的代碼中,如果參數長度添加局部長度大於256。這似乎沒有問題?
但是此函數將返回u8 類型的迭代器。
當函數join_() 中是function_view.parameters().len() 和function_view.locals().len() 組合值大於256,由於語句for local in self.iter_locals()中local 是u8類型,此時執行此語句對造成溢出。
實際上Move有校驗locals個數的過程,可惜在check bounds模塊只校驗locals,並沒有不包括參數length。
似乎開發人員知道這裡需要檢查參數+ 本地值(看註釋語句),然而代碼卻只校驗了本地變量的個數。
0x3 從Move整形溢出到DoS
通過上面的介紹,我們知道有一個主循環來掃描代碼塊,然後調用execute_block函數,之後會合併執行前後的state,當move代碼中存在循環,則會跳轉到代碼塊開始,再次執行基本塊。因此,如果我們製造一個循環代碼塊並利用溢出改變塊的state,使AbstractState 對像中的新的locals map與之前不同,當再次執行execute_block 函數時,在分析basic block中字節碼指令序列的時候會訪問新的locals map,這時候如果指令中需要訪問的索引在新的AbstractState locals map中不存在,將導致DoS。
在審核代碼後,我發現在reference safety模塊中,MoveLoc/CopyLoc/FreeRef 操作碼,我們可以實現這個目標。
這裡讓我們看一下文件路徑中execute_block函數調用的copy_loc函數作為一個說明:
move/language/move-bytecode-verifier/src/reference_safety/abstract_state.rs
在第287行,代碼嘗試通過LocalIndex作為參數獲取本地值,如果LocalIndex不存在會導致panic,想像一下當節點執行滿足上述條件代碼的時候,會導致整個節點崩潰。
0x4 PoC
下面是你可以在git裡面重現的PoC:
這是崩潰日誌:
thread 'regression_tests::reference_analysis::PoC' panicked at 'called `Option::unwrap()` on a `None` value', language/move-bytecode-verifier/src/reference_safety/abstract_state.rs:287:39
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
The DoS trigger Steps:
我們可以看到PoC中代碼塊存在一個basic block,分支指令是一個無條件分支指令,每次執行最後一條指令時,branch(0) 將跳回第一條指令,因此這個代碼塊將多次調用execute_block和join 函數。
1.在第一次執行完execute_block 函數,當這裡設置parameters 為SignatureIndex(0),locals為SignatureIndex(0)會導致num_locals為132*2=264。所以在執行join_函數下面這行代碼之後
for local in self.iter_locals()
will lead new locals length will be 264-256=8
將會導致新的locals map長度為8.
2.在第二次執行execute_block函數時,執行move代碼第一條指令copyloc(57),57是locals需要壓入棧的offset,但是這次locals只有長度8,offset 57不存在,所以會導致get (57).unwrap() 函數返回none ,最後導致panic。
0x5 總結
以上就是這個漏洞的來龍去脈。首先這個漏洞說明沒有絕對安全的代碼, Move語言在代碼執行之前確實做了很好的靜態校驗,但是就像這個漏洞一樣,可以通過溢出漏洞完全繞過之前的邊界校驗。再者代碼審計很重要,程序員難免會疏忽。作為Move語言安全研究的領導者,我們將繼續深挖Move的安全問題。第三點,對於Move語言,我們建議語言設計者在move運行時增加更多的檢查代碼,以防止意外情況的發生。目前move語言主要是在verify階段進行一系列的安全檢查,但我覺得這還不夠,一旦驗證被繞過,運行階段沒有過多的安全加固,將導致危害進一步加深,引發更嚴重的問題。最後,我們還發現了Move語言的另一個漏洞,後續會繼續分享給大家。