在本章中,我們將介紹原位符號執行的系統設計,分為三個部份:第一部份為運行 時期程序狀態提取,其中最初包括在程式中加入中斷點,還有程式運行時的程式碼、記 憶體、暫存器以及欲符號化的變數的擷取,以上等資訊皆須提取出來引入符號引擎中;
第二部份為符號引擎的執行,基於程序狀態提取的資料引入符號引擎中,利用符號執行 找到目標位置後,再紀錄符號引擎狀態的改變與符號變數的求解值;第三部份為狀態還 原,本系統會還原狀態與符號變數之值於原先執行的程式中,再觀察其是否能夠執行至 所預期的目標位置,以及後續程式的運行是否異常。
本系統主要分為兩大部份:第一部份為原位系統 (In-situ System),此處的行為模式 為具體執行,主要目的為暫停與繼續執行目標程序,以及將所需的程式碼和記憶體資訊 切割下來交給符號執行引擎使用;第二部份為符號執行引擎,此處為進行符號執行的區 域,將變數或是記憶體內容符號化使其能夠運行至目標位置,再求得符號化內容,最後 將結果同步回目標系統,讓系統可以順利運行。整體的系統架構如圖 4 所示。
7
圖 4. 原位符號執行系統架構圖
原位系統主要功能為暫停目標程序及擷取所求的片段程式碼,再記錄系統當下的狀 態,使用這些數據建構出可將片段程式符號化的效果,等符號執行完畢後才同步回具體 執行之中。在目標系統運行以前,我們會先利用逆向分析的方式找出此系統需要被符號 執行的區塊以及欲執行到的目標位置,而這些區塊須由一個至數個基本塊所組成。當系 統執行到此一區塊時會將此程序暫停,交由符號執行引擎去運作至所設定的目標位置,
最終將得到的結果返回至目標程序,使其可以順利執行到目標位置。
運行時期程序狀態的提取
在本研究中,我們使用ptrace 來完成運行時期程序狀態的提取,包含程式碼以及記 憶體內容的擷取。
程式碼擷取
系統開始運行前,我們會選取一個至數個中斷點作為欲進行符號執行的起始位置,
然後將中斷點設置於此。具體而言,我們會擷取出中斷點後一個位元組的資料,並將其
8
變更為0xCC 再放回去;0xCC 為一種軟體使用中斷點的方式 (softer breakpoint),又可 以稱為INT3。等到程式開始執行後,系統程序依序執行指令至 0xCC 時,CPU 會自動 判斷為中斷點位置,然後暫停程序。接著將原本的指令內容寫回記憶體,並且需要保留 執行當下的所有狀態,以利未來系統能夠繼續正常執行。暫停之後,將所有符號執行可 能會執行到的程式碼區段擷取下來,並記錄其儲存位址。最後需要標記出欲執行到的位 置,我們稱之為目標位置,而其起始位置與目標位置須事前經由追蹤程式的二進位檔案 獲取。
為了避免環境交互作用所會產生的副作用,我們希望起始位置與目標位置的範圍設 限於同一函數內,如程式的主函數中;並盡量避免觸碰到會呼叫系統的內核調用,還有 產生不必要的外力影響符號執行的正確性。一般的符號執行須引入程序中的所有物件,
難免會呼叫到內核調用的函數;但是原位符號執行僅需執行部分程式內容,所以可以選 擇避開這些複雜的系統呼叫內容,並將符號執行引擎高效利用於片段化的程式碼中。
記憶體內容擷取
得到所需片段程式碼後,為了將完整的系統建構起來,我們需要中斷點當下的執行 狀態,包括記憶體內容以及暫存器的資訊。記憶體的內容由於過於龐大我們難以一一拷 貝出來,而且大部份皆為沒有用的資料,對繼續執行有益的不外乎為stack 與 heap。stack 與 heap 通常存放著記憶體配置,包括各種變數還有返回值 (return address);而且兩者 的記憶體映射 (memory mapping) 方式相同,皆為找到起始位置後做映射,不過這些資 訊的起始位置會因為每次程式的重新執行而被放置於記憶體中不同位置,因此必須進入 系統層面查找;關於各個程序記憶體對映,我們以 ubuntu 18.0 為例,會記錄在 /proc/[PID]/maps 裡面,得到起始位置後可利用 ptrace 函數去做記憶體內容的提取。
stack 的內容也是極為龐大,但有用的資訊通常不超過數百個位元組,所以與其花 大量的時間去比對程式碼內容,得到實際會使用到的記憶體位置,我們採用的方法是直 接提取固定數量的位元組的stack 內容,而這大小可視實驗的系統的複雜度做修正。
9
接著需要得到的是暫存器的值,暫存器存放著當下執行指令、數據、位址,可視為 讀寫速度非常快的記憶體。其值利用ptrace 可簡單的求出,不過無法如記憶體內容般大 量的複製,需要對每個暫存器去做一一的輸出對映。
運行符號化執行引擎
在本研究中,我們採用的符號執行引擎為 angr,大部份的情況下使用 angr 都會引 入整個執行檔做分析,但我們採用的是片段程式碼追蹤與符號執行時的分析,angr 也提 供了可以載入部份程式的api:load_shellcode(),在這個 api 中僅須提供足夠的基本資訊 如程式碼內容、位移量與處理器的架構、arm、x86 等等即可。
雖然完成了符號執行運轉的基礎條件,但程序並非從頭開始跑,所以我們必須引入 暫存器的內容使符號執行引擎知曉須從哪個指令開始執行以及各大記憶體的存放位址;
除了暫存器外,程序狀態所擷取下的記憶體內容也需要在引擎內同步上去。
同步至符號執行引擎
我們將探討具體執行期間的狀態所提取的資料如何引入符號引擎。將接收到的資料 分成三部份:程式碼的內容、暫存器的值以及記憶體的資訊。程式碼的內容因為是從記 憶體中裁減出來的,所以資料會是以二進位的方式作為輸入,事實上在 angr 之中不論 是以二進位碼亦或是組譯語言皆會被轉為中間語言的形式 (Intermediate representation),
而其所用的中間語言為VEX,為 angr 用以優化符號執行引擎所使用的語言,目的是將 所有處理器架構的語言統一。VEX 的主要核心價值是表達每一條指令對程序產生的影 響、走過哪些路徑,如此對於在測試中幫助程序改變所走的路徑,以達到高覆蓋率。而 程式碼內容經過中間語言轉換後會導入至angr 的記憶體中按照給定的偏移量放置。
只要放置好程式內容,就可以使用符號引擎去執行各項操作,此時會給予程序一組 空白的起始狀態,為了使空白狀態的程序可以從擷取出的程式碼起始位置開始運作,我 們下個階段必須將程序的暫存器同步至符號執行引擎,否則缺少各項暫存器之值會導致 執行期間出錯,如rip 之值未正確設置會使得處理器無法正確導入下個指令,或 rbp 會 造成stack 的位置錯誤等等。
10
記憶體的內容同步方式與暫存器大同小異,需要重新打包載入,不同的是需打包成 大端序 (big-endian) 的形式,雖然記憶體內容是連續的,但打包時有長度限制,所以必 須利用迴圈的方式逐一完成。而資料在具體執行環境與符號執行引擎之間的轉換流程如 圖 5 所示。
圖 5. 資料轉換流程圖
符號化執行
在符號引擎同步化完成後,需要對剛剛已有狀態的程序創建一個模擬器,事實上 angr 可以同時對一個符號化位置的狀態進行多種模擬,並產生多個分支,不須從頭配置
11
狀態僅需創建多個模擬器即可,若同時有許多分支會運行至目標位置,為保證結果的正 確性,可以指定避免執行到的位址,而符號化執行的流程圖如圖 6 所示。
具體而言,創建出模擬器後對程序輸入目標位置,目標位置的查找是使用人工的方 式利用二進位檔逆向追蹤出來的,其目的為使 angr 可以確切的執行到目標位置。一開 始系統會先判斷每個分支的符號執行引擎最後是否會達到目標位置,若最後不會達到目 標位置則會刪除該狀態分支;反之則會繼續判斷是否會運行置避免被執行位址。若符號 執行引擎有經過避免執行到的位址則會刪除該狀態分支;反之最終可以走到目標位置則 代表有解,可經由求解器求解得出變數或記憶體內容的值,再代回原目標程序,亦可經 由判斷符號化位置是否被執行得出記憶體位址是否被程式觸發。
圖 6. 符號化執行流程圖
12
狀態還原
符號執行完成後會得到一系列的值,再代回原目標程序。許多簡單的解值可以使用 手動的方式輸入,但大多的結果可能包含不可視字元,若輸入錯誤的話會導致無法運行 至原先預期的目標位置。我們採用的方法為將符號化變數的記憶體位置與解值輸出成檔 案後經由具體執行的系統輸入。
具體執行的系統接收到輸入後便會從中斷點處繼續執行,直到到達目標位置,原則 上我們僅會還原符號變數的內容,並不會對暫存器或其餘的記憶體內容作更動,因此不 會產生額外崩潰的問題。到達目標位置後可以選擇重複進行原位符號執行,這種方法主 要是將大型的符號執行部分拆成多個小的符號執行運作,並降低複雜度。
13